mute territories
This commit is contained in:
		
							parent
							
								
									baee771d67
								
							
						
					
					
						commit
						214e863458
					
				@ -191,8 +191,12 @@ const activeOrMine = (me) => {
 | 
				
			|||||||
export const muteClause = me =>
 | 
					export const muteClause = me =>
 | 
				
			||||||
  me ? `NOT EXISTS (SELECT 1 FROM "Mute" WHERE "Mute"."muterId" = ${me.id} AND "Mute"."mutedId" = "Item"."userId")` : ''
 | 
					  me ? `NOT EXISTS (SELECT 1 FROM "Mute" WHERE "Mute"."muterId" = ${me.id} AND "Mute"."mutedId" = "Item"."userId")` : ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const subClause = (sub, num, table) => {
 | 
					const subClause = (sub, num, table, me) => {
 | 
				
			||||||
  return sub ? `${table ? `"${table}".` : ''}"subName" = $${num}` : ''
 | 
					  return sub
 | 
				
			||||||
 | 
					    ? `${table ? `"${table}".` : ''}"subName" = $${num}`
 | 
				
			||||||
 | 
					    : me
 | 
				
			||||||
 | 
					      ? `NOT EXISTS (SELECT 1 FROM "MuteSub" WHERE "MuteSub"."userId" = ${me.id} AND "MuteSub"."subName" = ${table ? `"${table}".` : ''}"subName")`
 | 
				
			||||||
 | 
					      : ''
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function filterClause (me, models, type) {
 | 
					export async function filterClause (me, models, type) {
 | 
				
			||||||
@ -341,7 +345,7 @@ export default {
 | 
				
			|||||||
              ${relationClause(type)}
 | 
					              ${relationClause(type)}
 | 
				
			||||||
              ${whereClause(
 | 
					              ${whereClause(
 | 
				
			||||||
                '"Item".created_at <= $1',
 | 
					                '"Item".created_at <= $1',
 | 
				
			||||||
                subClause(sub, 4, subClauseTable(type)),
 | 
					                subClause(sub, 4, subClauseTable(type), me),
 | 
				
			||||||
                activeOrMine(me),
 | 
					                activeOrMine(me),
 | 
				
			||||||
                await filterClause(me, models, type),
 | 
					                await filterClause(me, models, type),
 | 
				
			||||||
                typeClause(type),
 | 
					                typeClause(type),
 | 
				
			||||||
@ -366,7 +370,7 @@ export default {
 | 
				
			|||||||
              ${whereClause(
 | 
					              ${whereClause(
 | 
				
			||||||
                '"Item"."pinId" IS NULL',
 | 
					                '"Item"."pinId" IS NULL',
 | 
				
			||||||
                '"Item"."deletedAt" IS NULL',
 | 
					                '"Item"."deletedAt" IS NULL',
 | 
				
			||||||
                subClause(sub, 5, subClauseTable(type)),
 | 
					                subClause(sub, 5, subClauseTable(type), me),
 | 
				
			||||||
                typeClause(type),
 | 
					                typeClause(type),
 | 
				
			||||||
                whenClause(when, 'Item'),
 | 
					                whenClause(when, 'Item'),
 | 
				
			||||||
                await filterClause(me, models, type),
 | 
					                await filterClause(me, models, type),
 | 
				
			||||||
@ -386,7 +390,7 @@ export default {
 | 
				
			|||||||
              ${whereClause(
 | 
					              ${whereClause(
 | 
				
			||||||
                '"Item"."pinId" IS NULL',
 | 
					                '"Item"."pinId" IS NULL',
 | 
				
			||||||
                '"Item"."deletedAt" IS NULL',
 | 
					                '"Item"."deletedAt" IS NULL',
 | 
				
			||||||
                subClause(sub, 5, subClauseTable(type)),
 | 
					                subClause(sub, 5, subClauseTable(type), me),
 | 
				
			||||||
                typeClause(type),
 | 
					                typeClause(type),
 | 
				
			||||||
                whenClause(when, 'Item'),
 | 
					                whenClause(when, 'Item'),
 | 
				
			||||||
                await filterClause(me, models, type),
 | 
					                await filterClause(me, models, type),
 | 
				
			||||||
@ -443,7 +447,7 @@ export default {
 | 
				
			|||||||
                      '"Item"."deletedAt" IS NULL',
 | 
					                      '"Item"."deletedAt" IS NULL',
 | 
				
			||||||
                      '"Item"."parentId" IS NULL',
 | 
					                      '"Item"."parentId" IS NULL',
 | 
				
			||||||
                      '"Item".bio = false',
 | 
					                      '"Item".bio = false',
 | 
				
			||||||
                      subClause(sub, 3, 'Item', true),
 | 
					                      subClause(sub, 3, 'Item', me),
 | 
				
			||||||
                      muteClause(me))}
 | 
					                      muteClause(me))}
 | 
				
			||||||
                    ORDER BY rank DESC
 | 
					                    ORDER BY rank DESC
 | 
				
			||||||
                    OFFSET $1
 | 
					                    OFFSET $1
 | 
				
			||||||
@ -460,7 +464,7 @@ export default {
 | 
				
			|||||||
                      ${SELECT}
 | 
					                      ${SELECT}
 | 
				
			||||||
                      FROM "Item"
 | 
					                      FROM "Item"
 | 
				
			||||||
                      ${whereClause(
 | 
					                      ${whereClause(
 | 
				
			||||||
                        subClause(sub, 3, 'Item', true),
 | 
					                        subClause(sub, 3, 'Item', me),
 | 
				
			||||||
                        muteClause(me),
 | 
					                        muteClause(me),
 | 
				
			||||||
                        '"Item"."pinId" IS NULL',
 | 
					                        '"Item"."pinId" IS NULL',
 | 
				
			||||||
                        '"Item"."deletedAt" IS NULL',
 | 
					                        '"Item"."deletedAt" IS NULL',
 | 
				
			||||||
@ -858,6 +862,10 @@ export default {
 | 
				
			|||||||
        return null
 | 
					        return null
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (item.sub) {
 | 
				
			||||||
 | 
					        return item.sub
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return await models.sub.findUnique({ where: { name: item.subName || item.root?.subName } })
 | 
					      return await models.sub.findUnique({ where: { name: item.subName || item.root?.subName } })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    position: async (item, args, { models }) => {
 | 
					    position: async (item, args, { models }) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
import { GraphQLError } from 'graphql'
 | 
					import { GraphQLError } from 'graphql'
 | 
				
			||||||
import { dayMonthYearToDate } from '../../lib/time'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// this function makes america more secure apparently
 | 
					// this function makes america more secure apparently
 | 
				
			||||||
export default async function assertGofacYourself ({ models, headers, ip }) {
 | 
					export default async function assertGofacYourself ({ models, headers, ip }) {
 | 
				
			||||||
 | 
				
			|||||||
@ -65,30 +65,33 @@ export default {
 | 
				
			|||||||
    sub: async (parent, { name }, { models, me }) => {
 | 
					    sub: async (parent, { name }, { models, me }) => {
 | 
				
			||||||
      if (!name) return null
 | 
					      if (!name) return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (me && name === 'jobs') {
 | 
					 | 
				
			||||||
        models.user.update({
 | 
					 | 
				
			||||||
          where: {
 | 
					 | 
				
			||||||
            id: me.id
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          data: {
 | 
					 | 
				
			||||||
            lastCheckedJobs: new Date()
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }).catch(console.log)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return await models.sub.findUnique({
 | 
					      return await models.sub.findUnique({
 | 
				
			||||||
        where: {
 | 
					        where: {
 | 
				
			||||||
          name
 | 
					          name
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        include: {
 | 
				
			||||||
 | 
					          MuteSub: {
 | 
				
			||||||
 | 
					            where: {
 | 
				
			||||||
 | 
					              userId: Number(me.id)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    subs: async (parent, args, { models }) => {
 | 
					    subs: async (parent, args, { models, me }) => {
 | 
				
			||||||
      return await models.sub.findMany({
 | 
					      return await models.sub.findMany({
 | 
				
			||||||
        where: {
 | 
					        where: {
 | 
				
			||||||
          status: {
 | 
					          status: {
 | 
				
			||||||
            not: 'STOPPED'
 | 
					            not: 'STOPPED'
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        include: {
 | 
				
			||||||
 | 
					          MuteSub: {
 | 
				
			||||||
 | 
					            where: {
 | 
				
			||||||
 | 
					              userId: Number(me.id)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        orderBy: {
 | 
					        orderBy: {
 | 
				
			||||||
          name: 'asc'
 | 
					          name: 'asc'
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -158,6 +161,22 @@ export default {
 | 
				
			|||||||
        queries,
 | 
					        queries,
 | 
				
			||||||
        { models, lnd, hash, hmac, me, enforceFee: sub.billingCost })
 | 
					        { models, lnd, hash, hmac, me, enforceFee: sub.billingCost })
 | 
				
			||||||
      return results[1]
 | 
					      return results[1]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    toggleMuteSub: async (parent, { name }, { me, models }) => {
 | 
				
			||||||
 | 
					      if (!me) {
 | 
				
			||||||
 | 
					        throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const lookupData = { userId: Number(me.id), subName: name }
 | 
				
			||||||
 | 
					      const where = { userId_subName: lookupData }
 | 
				
			||||||
 | 
					      const existing = await models.muteSub.findUnique({ where })
 | 
				
			||||||
 | 
					      if (existing) {
 | 
				
			||||||
 | 
					        await models.muteSub.delete({ where })
 | 
				
			||||||
 | 
					        return false
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        await models.muteSub.create({ data: { ...lookupData } })
 | 
				
			||||||
 | 
					        return true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  Sub: {
 | 
					  Sub: {
 | 
				
			||||||
@ -166,6 +185,9 @@ export default {
 | 
				
			|||||||
        return sub.user
 | 
					        return sub.user
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return await models.user.findUnique({ where: { id: sub.userId } })
 | 
					      return await models.user.findUnique({ where: { id: sub.userId } })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    meMuteSub: async (sub, args, { models }) => {
 | 
				
			||||||
 | 
					      return sub.MuteSub.length > 0
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ export default gql`
 | 
				
			|||||||
      billingType: String!, billingAutoRenew: Boolean!,
 | 
					      billingType: String!, billingAutoRenew: Boolean!,
 | 
				
			||||||
      moderated: Boolean!, hash: String, hmac: String): Sub
 | 
					      moderated: Boolean!, hash: String, hmac: String): Sub
 | 
				
			||||||
    paySub(name: String!, hash: String, hmac: String): Sub
 | 
					    paySub(name: String!, hash: String, hmac: String): Sub
 | 
				
			||||||
 | 
					    toggleMuteSub(name: String!): Boolean!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  type Sub {
 | 
					  type Sub {
 | 
				
			||||||
@ -33,5 +34,6 @@ export default gql`
 | 
				
			|||||||
    status: String!
 | 
					    status: String!
 | 
				
			||||||
    moderated: Boolean!
 | 
					    moderated: Boolean!
 | 
				
			||||||
    moderatedCount: Int!
 | 
					    moderatedCount: Int!
 | 
				
			||||||
 | 
					    meMuteSub: Boolean!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,7 @@ export function WelcomeBanner () {
 | 
				
			|||||||
    setHidden(me?.privates?.hideWelcomeBanner || (!me && window.localStorage.getItem('hideWelcomeBanner')))
 | 
					    setHidden(me?.privates?.hideWelcomeBanner || (!me && window.localStorage.getItem('hideWelcomeBanner')))
 | 
				
			||||||
  }, [me?.privates?.hideWelcomeBanner])
 | 
					  }, [me?.privates?.hideWelcomeBanner])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (hidden) return
 | 
					  if (hidden) return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Alert className={styles.banner} key='info' variant='info' onClose={handleClose} dismissible>
 | 
					    <Alert className={styles.banner} key='info' variant='info' onClose={handleClose} dismissible>
 | 
				
			||||||
 | 
				
			|||||||
@ -197,7 +197,7 @@ export default function Comment ({
 | 
				
			|||||||
                />)}
 | 
					                />)}
 | 
				
			||||||
            {topLevel && (
 | 
					            {topLevel && (
 | 
				
			||||||
              <span className='d-flex ms-auto align-items-center'>
 | 
					              <span className='d-flex ms-auto align-items-center'>
 | 
				
			||||||
                <Share item={item} />
 | 
					                <Share title={item?.title} path={`/items/${item?.id}`} />
 | 
				
			||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -133,7 +133,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
 | 
				
			|||||||
      right={
 | 
					      right={
 | 
				
			||||||
        !noReply &&
 | 
					        !noReply &&
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            <Share item={item} />
 | 
					            <Share title={item?.title} path={`/items/${item?.id}`} />
 | 
				
			||||||
            <Toc text={item.text} />
 | 
					            <Toc text={item.text} />
 | 
				
			||||||
          </>
 | 
					          </>
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ import { useMe } from './me'
 | 
				
			|||||||
import DontLikeThisDropdownItem, { OutlawDropdownItem } from './dont-link-this'
 | 
					import DontLikeThisDropdownItem, { OutlawDropdownItem } from './dont-link-this'
 | 
				
			||||||
import BookmarkDropdownItem from './bookmark'
 | 
					import BookmarkDropdownItem from './bookmark'
 | 
				
			||||||
import SubscribeDropdownItem from './subscribe'
 | 
					import SubscribeDropdownItem from './subscribe'
 | 
				
			||||||
import { CopyLinkDropdownItem } from './share'
 | 
					import { CopyLinkDropdownItem, CrosspostDropdownItem } from './share'
 | 
				
			||||||
import Hat from './hat'
 | 
					import Hat from './hat'
 | 
				
			||||||
import { AD_USER_ID } from '../lib/constants'
 | 
					import { AD_USER_ID } from '../lib/constants'
 | 
				
			||||||
import ActionDropdown from './action-dropdown'
 | 
					import ActionDropdown from './action-dropdown'
 | 
				
			||||||
@ -149,11 +149,13 @@ export default function ItemInfo ({
 | 
				
			|||||||
          <Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
 | 
					          <Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
 | 
				
			||||||
            opentimestamp
 | 
					            opentimestamp
 | 
				
			||||||
          </Link>}
 | 
					          </Link>}
 | 
				
			||||||
        {me && item?.noteId && (
 | 
					        {item?.noteId && (
 | 
				
			||||||
          <Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener')}>
 | 
					          <Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener')}>
 | 
				
			||||||
            nostr note
 | 
					            nostr note
 | 
				
			||||||
          </Dropdown.Item>
 | 
					          </Dropdown.Item>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
 | 
					        {item?.mine && !item?.noteId &&
 | 
				
			||||||
 | 
					          <CrosspostDropdownItem item={item} />}
 | 
				
			||||||
        {me && !item.position &&
 | 
					        {me && !item.position &&
 | 
				
			||||||
          !item.mine && !item.deletedAt &&
 | 
					          !item.mine && !item.deletedAt &&
 | 
				
			||||||
          (item.meDontLikeSats > meTotalSats
 | 
					          (item.meDontLikeSats > meTotalSats
 | 
				
			||||||
 | 
				
			|||||||
@ -73,7 +73,7 @@ export default function ItemJob ({ item, toc, rank, children }) {
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {toc &&
 | 
					        {toc &&
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            <Share item={item} />
 | 
					            <Share title={item?.title} path={`/items/${item?.id}`} />
 | 
				
			||||||
            <Toc text={item.text} />
 | 
					            <Toc text={item.text} />
 | 
				
			||||||
          </>}
 | 
					          </>}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -8,42 +8,29 @@ import { useToast } from './toast'
 | 
				
			|||||||
import { SSR } from '../lib/constants'
 | 
					import { SSR } from '../lib/constants'
 | 
				
			||||||
import { callWithTimeout } from '../lib/nostr'
 | 
					import { callWithTimeout } from '../lib/nostr'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getShareUrl = (item, me) => {
 | 
					const referrurl = (ipath, me) => {
 | 
				
			||||||
  const path = `/items/${item?.id}${me ? `/r/${me.name}` : ''}`
 | 
					  const path = `${ipath}${me ? `/r/${me.name}` : ''}`
 | 
				
			||||||
  if (!SSR) {
 | 
					  if (!SSR) {
 | 
				
			||||||
    return `${window.location.protocol}//${window.location.host}${path}`
 | 
					    return `${window.location.protocol}//${window.location.host}${path}`
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return `https://stacker.news${path}`
 | 
					  return `https://stacker.news${path}`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Share ({ item }) {
 | 
					export default function Share ({ path, title, className = '' }) {
 | 
				
			||||||
  const me = useMe()
 | 
					  const me = useMe()
 | 
				
			||||||
  const crossposter = useCrossposter()
 | 
					 | 
				
			||||||
  const toaster = useToast()
 | 
					  const toaster = useToast()
 | 
				
			||||||
  const url = getShareUrl(item, me)
 | 
					  const url = referrurl(path, me)
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const mine = item?.user?.id === me?.id
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const [updateNoteId] = useMutation(
 | 
					 | 
				
			||||||
    gql`
 | 
					 | 
				
			||||||
      mutation updateNoteId($id: ID!, $noteId: String!) {
 | 
					 | 
				
			||||||
        updateNoteId(id: $id, noteId: $noteId) {
 | 
					 | 
				
			||||||
          id
 | 
					 | 
				
			||||||
          noteId
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }`
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return !SSR && navigator?.share
 | 
					  return !SSR && navigator?.share
 | 
				
			||||||
    ? (
 | 
					    ? (
 | 
				
			||||||
      <div className='ms-auto pointer d-flex align-items-center'>
 | 
					      <div className='ms-auto pointer d-flex align-items-center'>
 | 
				
			||||||
        <ShareIcon
 | 
					        <ShareIcon
 | 
				
			||||||
          width={20} height={20}
 | 
					          width={20} height={20}
 | 
				
			||||||
          className='mx-2 fill-grey theme'
 | 
					          className={`mx-2 fill-grey theme ${className}`}
 | 
				
			||||||
          onClick={async () => {
 | 
					          onClick={async () => {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
              await navigator.share({
 | 
					              await navigator.share({
 | 
				
			||||||
                title: item.title || '',
 | 
					                title: title || '',
 | 
				
			||||||
                text: '',
 | 
					                text: '',
 | 
				
			||||||
                url
 | 
					                url
 | 
				
			||||||
              })
 | 
					              })
 | 
				
			||||||
@ -57,9 +44,8 @@ export default function Share ({ item }) {
 | 
				
			|||||||
    : (
 | 
					    : (
 | 
				
			||||||
      <Dropdown align='end' className='ms-auto pointer  d-flex align-items-center' as='span'>
 | 
					      <Dropdown align='end' className='ms-auto pointer  d-flex align-items-center' as='span'>
 | 
				
			||||||
        <Dropdown.Toggle variant='success' id='dropdown-basic' as='a'>
 | 
					        <Dropdown.Toggle variant='success' id='dropdown-basic' as='a'>
 | 
				
			||||||
          <ShareIcon width={20} height={20} className='mx-2 fill-grey theme' />
 | 
					          <ShareIcon width={20} height={20} className={`mx-2 fill-grey theme ${className}`} />
 | 
				
			||||||
        </Dropdown.Toggle>
 | 
					        </Dropdown.Toggle>
 | 
				
			||||||
 | 
					 | 
				
			||||||
        <Dropdown.Menu>
 | 
					        <Dropdown.Menu>
 | 
				
			||||||
          <Dropdown.Item
 | 
					          <Dropdown.Item
 | 
				
			||||||
            onClick={async () => {
 | 
					            onClick={async () => {
 | 
				
			||||||
@ -74,43 +60,6 @@ export default function Share ({ item }) {
 | 
				
			|||||||
          >
 | 
					          >
 | 
				
			||||||
            copy link
 | 
					            copy link
 | 
				
			||||||
          </Dropdown.Item>
 | 
					          </Dropdown.Item>
 | 
				
			||||||
          {mine && !item?.noteId && (
 | 
					 | 
				
			||||||
            <Dropdown.Item
 | 
					 | 
				
			||||||
              onClick={async () => {
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                  const pubkey = await callWithTimeout(() => window.nostr.getPublicKey(), 5000)
 | 
					 | 
				
			||||||
                  if (!pubkey) {
 | 
					 | 
				
			||||||
                    throw new Error('not available')
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                } catch (e) {
 | 
					 | 
				
			||||||
                  toaster.danger(`Nostr extension error: ${e.message}`)
 | 
					 | 
				
			||||||
                  return
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                  if (item?.id) {
 | 
					 | 
				
			||||||
                    const crosspostResult = await crossposter({ ...item })
 | 
					 | 
				
			||||||
                    const noteId = crosspostResult?.noteId
 | 
					 | 
				
			||||||
                    if (noteId) {
 | 
					 | 
				
			||||||
                      await updateNoteId({
 | 
					 | 
				
			||||||
                        variables: {
 | 
					 | 
				
			||||||
                          id: item.id,
 | 
					 | 
				
			||||||
                          noteId
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      })
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    toaster.success('Crosspost successful')
 | 
					 | 
				
			||||||
                  } else {
 | 
					 | 
				
			||||||
                    toaster.warning('Item ID not available')
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                } catch (e) {
 | 
					 | 
				
			||||||
                  console.error(e)
 | 
					 | 
				
			||||||
                  toaster.danger('Crosspost failed')
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              crosspost to nostr
 | 
					 | 
				
			||||||
            </Dropdown.Item>
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
        </Dropdown.Menu>
 | 
					        </Dropdown.Menu>
 | 
				
			||||||
      </Dropdown>)
 | 
					      </Dropdown>)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -118,7 +67,7 @@ export default function Share ({ item }) {
 | 
				
			|||||||
export function CopyLinkDropdownItem ({ item }) {
 | 
					export function CopyLinkDropdownItem ({ item }) {
 | 
				
			||||||
  const me = useMe()
 | 
					  const me = useMe()
 | 
				
			||||||
  const toaster = useToast()
 | 
					  const toaster = useToast()
 | 
				
			||||||
  const url = getShareUrl(item, me)
 | 
					  const url = referrurl(`/items/${item.id}`, me)
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Dropdown.Item
 | 
					    <Dropdown.Item
 | 
				
			||||||
      onClick={async () => {
 | 
					      onClick={async () => {
 | 
				
			||||||
@ -143,3 +92,56 @@ export function CopyLinkDropdownItem ({ item }) {
 | 
				
			|||||||
    </Dropdown.Item>
 | 
					    </Dropdown.Item>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function CrosspostDropdownItem ({ item }) {
 | 
				
			||||||
 | 
					  const crossposter = useCrossposter()
 | 
				
			||||||
 | 
					  const toaster = useToast()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [updateNoteId] = useMutation(
 | 
				
			||||||
 | 
					    gql`
 | 
				
			||||||
 | 
					      mutation updateNoteId($id: ID!, $noteId: String!) {
 | 
				
			||||||
 | 
					        updateNoteId(id: $id, noteId: $noteId) {
 | 
				
			||||||
 | 
					          id
 | 
				
			||||||
 | 
					          noteId
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }`
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dropdown.Item
 | 
				
			||||||
 | 
					      onClick={async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const pubkey = await callWithTimeout(() => window.nostr.getPublicKey(), 5000)
 | 
				
			||||||
 | 
					          if (!pubkey) {
 | 
				
			||||||
 | 
					            throw new Error('not available')
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          toaster.danger(`Nostr extension error: ${e.message}`)
 | 
				
			||||||
 | 
					          return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          if (item?.id) {
 | 
				
			||||||
 | 
					            const crosspostResult = await crossposter({ ...item })
 | 
				
			||||||
 | 
					            const noteId = crosspostResult?.noteId
 | 
				
			||||||
 | 
					            if (noteId) {
 | 
				
			||||||
 | 
					              await updateNoteId({
 | 
				
			||||||
 | 
					                variables: {
 | 
				
			||||||
 | 
					                  id: item.id,
 | 
				
			||||||
 | 
					                  noteId
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            toaster.success('Crosspost successful')
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            toaster.warning('Item ID not available')
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          console.error(e)
 | 
				
			||||||
 | 
					          toaster.danger('Crosspost failed')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      crosspost to nostr
 | 
				
			||||||
 | 
					    </Dropdown.Item>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										92
									
								
								components/territory-header.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								components/territory-header.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					import { Badge, Button, CardFooter } from 'react-bootstrap'
 | 
				
			||||||
 | 
					import { AccordianCard } from './accordian-item'
 | 
				
			||||||
 | 
					import TerritoryPaymentDue, { TerritoryBillingLine } from './territory-payment-due'
 | 
				
			||||||
 | 
					import Link from 'next/link'
 | 
				
			||||||
 | 
					import Text from './text'
 | 
				
			||||||
 | 
					import { numWithUnits } from '../lib/format'
 | 
				
			||||||
 | 
					import styles from './item.module.css'
 | 
				
			||||||
 | 
					import Hat from './hat'
 | 
				
			||||||
 | 
					import { useMe } from './me'
 | 
				
			||||||
 | 
					import Share from './share'
 | 
				
			||||||
 | 
					import { gql, useMutation } from '@apollo/client'
 | 
				
			||||||
 | 
					import { useToast } from './toast'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function TerritoryHeader ({ sub }) {
 | 
				
			||||||
 | 
					  const me = useMe()
 | 
				
			||||||
 | 
					  const toaster = useToast()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [toggleMuteSub] = useMutation(
 | 
				
			||||||
 | 
					    gql`
 | 
				
			||||||
 | 
					      mutation toggleMuteSub($name: String!) {
 | 
				
			||||||
 | 
					        toggleMuteSub(name: $name)
 | 
				
			||||||
 | 
					      }`, {
 | 
				
			||||||
 | 
					      update (cache, { data: { toggleMuteSub } }) {
 | 
				
			||||||
 | 
					        cache.modify({
 | 
				
			||||||
 | 
					          id: `Sub:{"name":"${sub.name}"}`,
 | 
				
			||||||
 | 
					          fields: {
 | 
				
			||||||
 | 
					            meMuteSub: () => toggleMuteSub.meMuteSub
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <TerritoryPaymentDue sub={sub} />
 | 
				
			||||||
 | 
					      <div className='mb-3'>
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <AccordianCard
 | 
				
			||||||
 | 
					            header={
 | 
				
			||||||
 | 
					              <small className='text-muted fw-bold align-items-center d-flex'>
 | 
				
			||||||
 | 
					                territory details
 | 
				
			||||||
 | 
					                {sub.status === 'STOPPED' && <Badge className='ms-2' bg='danger'>archived</Badge>}
 | 
				
			||||||
 | 
					                {(sub.moderated || sub.moderatedCount > 0) && <Badge className='ms-2' bg='secondary'>moderated{sub.moderatedCount && ` ${sub.moderatedCount}`}</Badge>}
 | 
				
			||||||
 | 
					              </small>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div className='py-2'>
 | 
				
			||||||
 | 
					              <Text>{sub.desc}</Text>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <CardFooter className={`py-1 ${styles.other}`}>
 | 
				
			||||||
 | 
					              <div className='text-muted'>
 | 
				
			||||||
 | 
					                <span>founded by </span>
 | 
				
			||||||
 | 
					                <Link href={`/${sub.user.name}`}>
 | 
				
			||||||
 | 
					                  @{sub.user.name}<span> </span><Hat className='fill-grey' user={sub.user} height={12} width={12} />
 | 
				
			||||||
 | 
					                </Link>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div className='text-muted'>
 | 
				
			||||||
 | 
					                <span>post cost </span>
 | 
				
			||||||
 | 
					                <span className='fw-bold'>{numWithUnits(sub.baseCost)}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <TerritoryBillingLine sub={sub} />
 | 
				
			||||||
 | 
					            </CardFooter>
 | 
				
			||||||
 | 
					          </AccordianCard>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className='d-flex my-2 justify-content-end'>
 | 
				
			||||||
 | 
					          <Share path={`/~${sub.name}`} title={`~${sub.name} stacker news territory`} className='mx-3' />
 | 
				
			||||||
 | 
					          {Number(sub.userId) === Number(me?.id)
 | 
				
			||||||
 | 
					            ? (
 | 
				
			||||||
 | 
					              <Link href={`/~${sub.name}/edit`} className='d-flex align-items-center'>
 | 
				
			||||||
 | 
					                <Button variant='outline-grey border-2 rounded py-0' size='sm'>edit territory</Button>
 | 
				
			||||||
 | 
					              </Link>)
 | 
				
			||||||
 | 
					            : (
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                variant='outline-grey border-2 py-0 rounded'
 | 
				
			||||||
 | 
					                size='sm'
 | 
				
			||||||
 | 
					                onClick={async () => {
 | 
				
			||||||
 | 
					                  try {
 | 
				
			||||||
 | 
					                    await toggleMuteSub({ variables: { name: sub.name } })
 | 
				
			||||||
 | 
					                  } catch {
 | 
				
			||||||
 | 
					                    toaster.danger(`failed to ${sub.meMuteSub ? 'join' : 'mute'} territory`)
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                  toaster.success(`${sub.meMuteSub ? 'joined' : 'muted'} territory`)
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >{sub.meMuteSub ? 'join' : 'mute'} territory
 | 
				
			||||||
 | 
					              </Button>)}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -18,6 +18,7 @@ export const SUB_FIELDS = gql`
 | 
				
			|||||||
    status
 | 
					    status
 | 
				
			||||||
    moderated
 | 
					    moderated
 | 
				
			||||||
    moderatedCount
 | 
					    moderatedCount
 | 
				
			||||||
 | 
					    meMuteSub
 | 
				
			||||||
  }`
 | 
					  }`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SUB_FULL_FIELDS = gql`
 | 
					export const SUB_FULL_FIELDS = gql`
 | 
				
			||||||
 | 
				
			|||||||
@ -5,19 +5,9 @@ import Layout from '../../components/layout'
 | 
				
			|||||||
import { SUB_FULL, SUB_ITEMS } from '../../fragments/subs'
 | 
					import { SUB_FULL, SUB_ITEMS } from '../../fragments/subs'
 | 
				
			||||||
import Snl from '../../components/snl'
 | 
					import Snl from '../../components/snl'
 | 
				
			||||||
import { WelcomeBanner } from '../../components/banners'
 | 
					import { WelcomeBanner } from '../../components/banners'
 | 
				
			||||||
import { AccordianCard } from '../../components/accordian-item'
 | 
					 | 
				
			||||||
import Text from '../../components/text'
 | 
					 | 
				
			||||||
import { useMe } from '../../components/me'
 | 
					 | 
				
			||||||
import Gear from '../../svgs/settings-5-fill.svg'
 | 
					 | 
				
			||||||
import Link from 'next/link'
 | 
					 | 
				
			||||||
import { useQuery } from '@apollo/client'
 | 
					import { useQuery } from '@apollo/client'
 | 
				
			||||||
import PageLoading from '../../components/page-loading'
 | 
					import PageLoading from '../../components/page-loading'
 | 
				
			||||||
import CardFooter from 'react-bootstrap/CardFooter'
 | 
					import TerritoryHeader from '../../components/territory-header'
 | 
				
			||||||
import Hat from '../../components/hat'
 | 
					 | 
				
			||||||
import styles from '../../components/item.module.css'
 | 
					 | 
				
			||||||
import TerritoryPaymentDue, { TerritoryBillingLine } from '../../components/territory-payment-due'
 | 
					 | 
				
			||||||
import Badge from 'react-bootstrap/Badge'
 | 
					 | 
				
			||||||
import { numWithUnits } from '../../lib/format'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({
 | 
					export const getServerSideProps = getGetServerSideProps({
 | 
				
			||||||
  query: SUB_ITEMS,
 | 
					  query: SUB_ITEMS,
 | 
				
			||||||
@ -26,7 +16,6 @@ export const getServerSideProps = getGetServerSideProps({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default function Sub ({ ssrData }) {
 | 
					export default function Sub ({ ssrData }) {
 | 
				
			||||||
  const router = useRouter()
 | 
					  const router = useRouter()
 | 
				
			||||||
  const me = useMe()
 | 
					 | 
				
			||||||
  const variables = { ...router.query }
 | 
					  const variables = { ...router.query }
 | 
				
			||||||
  const { data } = useQuery(SUB_FULL, { variables })
 | 
					  const { data } = useQuery(SUB_FULL, { variables })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -36,44 +25,7 @@ export default function Sub ({ ssrData }) {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Layout sub={variables.sub}>
 | 
					    <Layout sub={variables.sub}>
 | 
				
			||||||
      {sub
 | 
					      {sub
 | 
				
			||||||
        ? (
 | 
					        ? <TerritoryHeader sub={sub} />
 | 
				
			||||||
          <>
 | 
					 | 
				
			||||||
            <TerritoryPaymentDue sub={sub} />
 | 
					 | 
				
			||||||
            <div className='mb-3 d-flex'>
 | 
					 | 
				
			||||||
              <div className='flex-grow-1'>
 | 
					 | 
				
			||||||
                <AccordianCard
 | 
					 | 
				
			||||||
                  header={
 | 
					 | 
				
			||||||
                    <small className='text-muted fw-bold align-items-center d-flex'>
 | 
					 | 
				
			||||||
                      territory details
 | 
					 | 
				
			||||||
                      {sub.status === 'STOPPED' && <Badge className='ms-2' bg='danger'>archived</Badge>}
 | 
					 | 
				
			||||||
                      {(sub.moderated || sub.moderatedCount) && <Badge className='ms-2' bg='secondary'>moderated{sub.moderatedCount && ` ${sub.moderatedCount}`}</Badge>}
 | 
					 | 
				
			||||||
                    </small>
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <div className='py-2'>
 | 
					 | 
				
			||||||
                    <Text>{sub.desc}</Text>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                  <CardFooter className={`py-1 ${styles.other}`}>
 | 
					 | 
				
			||||||
                    <div className='text-muted'>
 | 
					 | 
				
			||||||
                      <span>founded by </span>
 | 
					 | 
				
			||||||
                      <Link href={`/${sub.user.name}`}>
 | 
					 | 
				
			||||||
                        @{sub.user.name}<span> </span><Hat className='fill-grey' user={sub.user} height={12} width={12} />
 | 
					 | 
				
			||||||
                      </Link>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    <div className='text-muted'>
 | 
					 | 
				
			||||||
                      <span>post cost </span>
 | 
					 | 
				
			||||||
                      <span className='fw-bold'>{numWithUnits(sub.baseCost)}</span>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    <TerritoryBillingLine sub={sub} />
 | 
					 | 
				
			||||||
                  </CardFooter>
 | 
					 | 
				
			||||||
                </AccordianCard>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
              {Number(sub.userId) === Number(me?.id) &&
 | 
					 | 
				
			||||||
                <Link href={`/~${sub.name}/edit`} className='d-flex align-items-center flex-shrink-1 ps-2'>
 | 
					 | 
				
			||||||
                  <Gear className='fill-grey' width={22} height={22} />
 | 
					 | 
				
			||||||
                </Link>}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </>)
 | 
					 | 
				
			||||||
        : (
 | 
					        : (
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            <Snl />
 | 
					            <Snl />
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										36
									
								
								prisma/migrations/20231230015539_mute_sub/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								prisma/migrations/20231230015539_mute_sub/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					  Warnings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - You are about to drop the `Subscription` table. If the table is not empty, all the data it contains will be lost.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					-- DropForeignKey
 | 
				
			||||||
 | 
					ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_subName_fkey";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- DropForeignKey
 | 
				
			||||||
 | 
					ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_userId_fkey";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- DropTable
 | 
				
			||||||
 | 
					DROP TABLE "Subscription";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- CreateTable
 | 
				
			||||||
 | 
					CREATE TABLE "MuteSub" (
 | 
				
			||||||
 | 
					    "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
					    "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
					    "subName" CITEXT NOT NULL,
 | 
				
			||||||
 | 
					    "userId" INTEGER NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    CONSTRAINT "MuteSub_pkey" PRIMARY KEY ("userId","subName")
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- CreateIndex
 | 
				
			||||||
 | 
					CREATE INDEX "MuteSub_subName_idx" ON "MuteSub"("subName");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- CreateIndex
 | 
				
			||||||
 | 
					CREATE INDEX "MuteSub_created_at_idx" ON "MuteSub"("created_at");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AddForeignKey
 | 
				
			||||||
 | 
					ALTER TABLE "MuteSub" ADD CONSTRAINT "MuteSub_subName_fkey" FOREIGN KEY ("subName") REFERENCES "Sub"("name") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AddForeignKey
 | 
				
			||||||
 | 
					ALTER TABLE "MuteSub" ADD CONSTRAINT "MuteSub_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
				
			||||||
@ -77,7 +77,6 @@ model User {
 | 
				
			|||||||
  PushSubscriptions     PushSubscription[]
 | 
					  PushSubscriptions     PushSubscription[]
 | 
				
			||||||
  ReferralAct           ReferralAct[]
 | 
					  ReferralAct           ReferralAct[]
 | 
				
			||||||
  Streak                Streak[]
 | 
					  Streak                Streak[]
 | 
				
			||||||
  Subscriptions         Subscription[]
 | 
					 | 
				
			||||||
  ThreadSubscriptions   ThreadSubscription[]
 | 
					  ThreadSubscriptions   ThreadSubscription[]
 | 
				
			||||||
  Upload                Upload[]             @relation("Uploads")
 | 
					  Upload                Upload[]             @relation("Uploads")
 | 
				
			||||||
  nostrRelays           UserNostrRelay[]
 | 
					  nostrRelays           UserNostrRelay[]
 | 
				
			||||||
@ -102,6 +101,7 @@ model User {
 | 
				
			|||||||
  ArcIn                 Arc[]                @relation("toUser")
 | 
					  ArcIn                 Arc[]                @relation("toUser")
 | 
				
			||||||
  Sub                   Sub[]
 | 
					  Sub                   Sub[]
 | 
				
			||||||
  SubAct                SubAct[]
 | 
					  SubAct                SubAct[]
 | 
				
			||||||
 | 
					  MuteSub               MuteSub[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @@index([photoId])
 | 
					  @@index([photoId])
 | 
				
			||||||
  @@index([createdAt], map: "users.created_at_index")
 | 
					  @@index([createdAt], map: "users.created_at_index")
 | 
				
			||||||
@ -419,12 +419,12 @@ model Sub {
 | 
				
			|||||||
  moderated        Boolean     @default(false)
 | 
					  moderated        Boolean     @default(false)
 | 
				
			||||||
  moderatedCount   Int         @default(0)
 | 
					  moderatedCount   Int         @default(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  parent       Sub?           @relation("ParentChildren", fields: [parentName], references: [name])
 | 
					  parent   Sub?      @relation("ParentChildren", fields: [parentName], references: [name])
 | 
				
			||||||
  children     Sub[]          @relation("ParentChildren")
 | 
					  children Sub[]     @relation("ParentChildren")
 | 
				
			||||||
  user         User           @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
					  user     User      @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
  Item         Item[]
 | 
					  Item     Item[]
 | 
				
			||||||
  Subscription Subscription[]
 | 
					  SubAct   SubAct[]
 | 
				
			||||||
  SubAct       SubAct[]
 | 
					  MuteSub  MuteSub[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @@index([parentName])
 | 
					  @@index([parentName])
 | 
				
			||||||
  @@index([createdAt])
 | 
					  @@index([createdAt])
 | 
				
			||||||
@ -451,14 +451,17 @@ model SubAct {
 | 
				
			|||||||
  @@index([userId, createdAt, type])
 | 
					  @@index([userId, createdAt, type])
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Subscription {
 | 
					model MuteSub {
 | 
				
			||||||
  id        Int      @id @default(autoincrement())
 | 
					 | 
				
			||||||
  createdAt DateTime @default(now()) @map("created_at")
 | 
					  createdAt DateTime @default(now()) @map("created_at")
 | 
				
			||||||
  updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
 | 
					  updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
 | 
				
			||||||
  subName   String   @db.Citext
 | 
					  subName   String   @db.Citext
 | 
				
			||||||
  userId    Int
 | 
					  userId    Int
 | 
				
			||||||
  sub       Sub      @relation(fields: [subName], references: [name], onDelete: Cascade)
 | 
					  sub       Sub      @relation(fields: [subName], references: [name], onDelete: Cascade)
 | 
				
			||||||
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
					  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@id([userId, subName])
 | 
				
			||||||
 | 
					  @@index([subName])
 | 
				
			||||||
 | 
					  @@index([createdAt])
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Pin {
 | 
					model Pin {
 | 
				
			||||||
 | 
				
			|||||||
@ -187,6 +187,23 @@ $accordion-button-active-icon-dark:  $accordion-button-icon;
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-outline-grey {
 | 
				
			||||||
 | 
					  --bs-btn-color: var(--theme-grey);
 | 
				
			||||||
 | 
					  --bs-btn-border-color: var(--theme-grey);
 | 
				
			||||||
 | 
					  --bs-btn-hover-color: #fff;
 | 
				
			||||||
 | 
					  --bs-btn-hover-bg: var(--theme-grey);
 | 
				
			||||||
 | 
					  --bs-btn-hover-border-color: var(--theme-grey);
 | 
				
			||||||
 | 
					  --bs-btn-focus-shadow-rgb: 233, 236, 239;
 | 
				
			||||||
 | 
					  --bs-btn-active-color: #fff;
 | 
				
			||||||
 | 
					  --bs-btn-active-bg: var(--theme-grey);
 | 
				
			||||||
 | 
					  --bs-btn-active-border-color: var(--theme-grey);
 | 
				
			||||||
 | 
					  --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
 | 
				
			||||||
 | 
					  --bs-btn-disabled-color: var(--theme-grey);
 | 
				
			||||||
 | 
					  --bs-btn-disabled-bg: transparent;
 | 
				
			||||||
 | 
					  --bs-btn-disabled-border-color: var(--theme-grey);
 | 
				
			||||||
 | 
					  --bs-gradient: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.text-monospace {
 | 
					.text-monospace {
 | 
				
			||||||
  font-family: monospace;
 | 
					  font-family: monospace;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user