mute territories
This commit is contained in:
parent
baee771d67
commit
214e863458
|
@ -191,8 +191,12 @@ const activeOrMine = (me) => {
|
|||
export const muteClause = me =>
|
||||
me ? `NOT EXISTS (SELECT 1 FROM "Mute" WHERE "Mute"."muterId" = ${me.id} AND "Mute"."mutedId" = "Item"."userId")` : ''
|
||||
|
||||
const subClause = (sub, num, table) => {
|
||||
return sub ? `${table ? `"${table}".` : ''}"subName" = $${num}` : ''
|
||||
const subClause = (sub, num, table, me) => {
|
||||
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) {
|
||||
|
@ -341,7 +345,7 @@ export default {
|
|||
${relationClause(type)}
|
||||
${whereClause(
|
||||
'"Item".created_at <= $1',
|
||||
subClause(sub, 4, subClauseTable(type)),
|
||||
subClause(sub, 4, subClauseTable(type), me),
|
||||
activeOrMine(me),
|
||||
await filterClause(me, models, type),
|
||||
typeClause(type),
|
||||
|
@ -366,7 +370,7 @@ export default {
|
|||
${whereClause(
|
||||
'"Item"."pinId" IS NULL',
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
subClause(sub, 5, subClauseTable(type)),
|
||||
subClause(sub, 5, subClauseTable(type), me),
|
||||
typeClause(type),
|
||||
whenClause(when, 'Item'),
|
||||
await filterClause(me, models, type),
|
||||
|
@ -386,7 +390,7 @@ export default {
|
|||
${whereClause(
|
||||
'"Item"."pinId" IS NULL',
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
subClause(sub, 5, subClauseTable(type)),
|
||||
subClause(sub, 5, subClauseTable(type), me),
|
||||
typeClause(type),
|
||||
whenClause(when, 'Item'),
|
||||
await filterClause(me, models, type),
|
||||
|
@ -443,7 +447,7 @@ export default {
|
|||
'"Item"."deletedAt" IS NULL',
|
||||
'"Item"."parentId" IS NULL',
|
||||
'"Item".bio = false',
|
||||
subClause(sub, 3, 'Item', true),
|
||||
subClause(sub, 3, 'Item', me),
|
||||
muteClause(me))}
|
||||
ORDER BY rank DESC
|
||||
OFFSET $1
|
||||
|
@ -460,7 +464,7 @@ export default {
|
|||
${SELECT}
|
||||
FROM "Item"
|
||||
${whereClause(
|
||||
subClause(sub, 3, 'Item', true),
|
||||
subClause(sub, 3, 'Item', me),
|
||||
muteClause(me),
|
||||
'"Item"."pinId" IS NULL',
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
|
@ -858,6 +862,10 @@ export default {
|
|||
return null
|
||||
}
|
||||
|
||||
if (item.sub) {
|
||||
return item.sub
|
||||
}
|
||||
|
||||
return await models.sub.findUnique({ where: { name: item.subName || item.root?.subName } })
|
||||
},
|
||||
position: async (item, args, { models }) => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GraphQLError } from 'graphql'
|
||||
import { dayMonthYearToDate } from '../../lib/time'
|
||||
|
||||
// this function makes america more secure apparently
|
||||
export default async function assertGofacYourself ({ models, headers, ip }) {
|
||||
|
|
|
@ -65,30 +65,33 @@ export default {
|
|||
sub: async (parent, { name }, { models, me }) => {
|
||||
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({
|
||||
where: {
|
||||
name
|
||||
},
|
||||
include: {
|
||||
MuteSub: {
|
||||
where: {
|
||||
userId: Number(me.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
subs: async (parent, args, { models }) => {
|
||||
subs: async (parent, args, { models, me }) => {
|
||||
return await models.sub.findMany({
|
||||
where: {
|
||||
status: {
|
||||
not: 'STOPPED'
|
||||
}
|
||||
},
|
||||
include: {
|
||||
MuteSub: {
|
||||
where: {
|
||||
userId: Number(me.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
|
@ -158,6 +161,22 @@ export default {
|
|||
queries,
|
||||
{ models, lnd, hash, hmac, me, enforceFee: sub.billingCost })
|
||||
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: {
|
||||
|
@ -166,6 +185,9 @@ export default {
|
|||
return sub.user
|
||||
}
|
||||
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!,
|
||||
moderated: Boolean!, hash: String, hmac: String): Sub
|
||||
paySub(name: String!, hash: String, hmac: String): Sub
|
||||
toggleMuteSub(name: String!): Boolean!
|
||||
}
|
||||
|
||||
type Sub {
|
||||
|
@ -33,5 +34,6 @@ export default gql`
|
|||
status: String!
|
||||
moderated: Boolean!
|
||||
moderatedCount: Int!
|
||||
meMuteSub: Boolean!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -40,7 +40,7 @@ export function WelcomeBanner () {
|
|||
setHidden(me?.privates?.hideWelcomeBanner || (!me && window.localStorage.getItem('hideWelcomeBanner')))
|
||||
}, [me?.privates?.hideWelcomeBanner])
|
||||
|
||||
if (hidden) return
|
||||
if (hidden) return null
|
||||
|
||||
return (
|
||||
<Alert className={styles.banner} key='info' variant='info' onClose={handleClose} dismissible>
|
||||
|
|
|
@ -197,7 +197,7 @@ export default function Comment ({
|
|||
/>)}
|
||||
{topLevel && (
|
||||
<span className='d-flex ms-auto align-items-center'>
|
||||
<Share item={item} />
|
||||
<Share title={item?.title} path={`/items/${item?.id}`} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -133,7 +133,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
|||
right={
|
||||
!noReply &&
|
||||
<>
|
||||
<Share item={item} />
|
||||
<Share title={item?.title} path={`/items/${item?.id}`} />
|
||||
<Toc text={item.text} />
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useMe } from './me'
|
|||
import DontLikeThisDropdownItem, { OutlawDropdownItem } from './dont-link-this'
|
||||
import BookmarkDropdownItem from './bookmark'
|
||||
import SubscribeDropdownItem from './subscribe'
|
||||
import { CopyLinkDropdownItem } from './share'
|
||||
import { CopyLinkDropdownItem, CrosspostDropdownItem } from './share'
|
||||
import Hat from './hat'
|
||||
import { AD_USER_ID } from '../lib/constants'
|
||||
import ActionDropdown from './action-dropdown'
|
||||
|
@ -149,11 +149,13 @@ export default function ItemInfo ({
|
|||
<Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
|
||||
opentimestamp
|
||||
</Link>}
|
||||
{me && item?.noteId && (
|
||||
{item?.noteId && (
|
||||
<Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener')}>
|
||||
nostr note
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{item?.mine && !item?.noteId &&
|
||||
<CrosspostDropdownItem item={item} />}
|
||||
{me && !item.position &&
|
||||
!item.mine && !item.deletedAt &&
|
||||
(item.meDontLikeSats > meTotalSats
|
||||
|
|
|
@ -73,7 +73,7 @@ export default function ItemJob ({ item, toc, rank, children }) {
|
|||
</div>
|
||||
{toc &&
|
||||
<>
|
||||
<Share item={item} />
|
||||
<Share title={item?.title} path={`/items/${item?.id}`} />
|
||||
<Toc text={item.text} />
|
||||
</>}
|
||||
</div>
|
||||
|
|
|
@ -8,42 +8,29 @@ import { useToast } from './toast'
|
|||
import { SSR } from '../lib/constants'
|
||||
import { callWithTimeout } from '../lib/nostr'
|
||||
|
||||
const getShareUrl = (item, me) => {
|
||||
const path = `/items/${item?.id}${me ? `/r/${me.name}` : ''}`
|
||||
const referrurl = (ipath, me) => {
|
||||
const path = `${ipath}${me ? `/r/${me.name}` : ''}`
|
||||
if (!SSR) {
|
||||
return `${window.location.protocol}//${window.location.host}${path}`
|
||||
}
|
||||
return `https://stacker.news${path}`
|
||||
}
|
||||
|
||||
export default function Share ({ item }) {
|
||||
export default function Share ({ path, title, className = '' }) {
|
||||
const me = useMe()
|
||||
const crossposter = useCrossposter()
|
||||
const toaster = useToast()
|
||||
const url = getShareUrl(item, 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
|
||||
}
|
||||
}`
|
||||
)
|
||||
const url = referrurl(path, me)
|
||||
|
||||
return !SSR && navigator?.share
|
||||
? (
|
||||
<div className='ms-auto pointer d-flex align-items-center'>
|
||||
<ShareIcon
|
||||
width={20} height={20}
|
||||
className='mx-2 fill-grey theme'
|
||||
className={`mx-2 fill-grey theme ${className}`}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: item.title || '',
|
||||
title: title || '',
|
||||
text: '',
|
||||
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.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.Menu>
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
|
@ -74,43 +60,6 @@ export default function Share ({ item }) {
|
|||
>
|
||||
copy link
|
||||
</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>)
|
||||
}
|
||||
|
@ -118,7 +67,7 @@ export default function Share ({ item }) {
|
|||
export function CopyLinkDropdownItem ({ item }) {
|
||||
const me = useMe()
|
||||
const toaster = useToast()
|
||||
const url = getShareUrl(item, me)
|
||||
const url = referrurl(`/items/${item.id}`, me)
|
||||
return (
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
|
@ -143,3 +92,56 @@ export function CopyLinkDropdownItem ({ 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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
moderated
|
||||
moderatedCount
|
||||
meMuteSub
|
||||
}`
|
||||
|
||||
export const SUB_FULL_FIELDS = gql`
|
||||
|
|
|
@ -5,19 +5,9 @@ import Layout from '../../components/layout'
|
|||
import { SUB_FULL, SUB_ITEMS } from '../../fragments/subs'
|
||||
import Snl from '../../components/snl'
|
||||
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 PageLoading from '../../components/page-loading'
|
||||
import CardFooter from 'react-bootstrap/CardFooter'
|
||||
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'
|
||||
import TerritoryHeader from '../../components/territory-header'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({
|
||||
query: SUB_ITEMS,
|
||||
|
@ -26,7 +16,6 @@ export const getServerSideProps = getGetServerSideProps({
|
|||
|
||||
export default function Sub ({ ssrData }) {
|
||||
const router = useRouter()
|
||||
const me = useMe()
|
||||
const variables = { ...router.query }
|
||||
const { data } = useQuery(SUB_FULL, { variables })
|
||||
|
||||
|
@ -36,44 +25,7 @@ export default function Sub ({ ssrData }) {
|
|||
return (
|
||||
<Layout sub={variables.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>
|
||||
</>)
|
||||
? <TerritoryHeader sub={sub} />
|
||||
: (
|
||||
<>
|
||||
<Snl />
|
||||
|
|
|
@ -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[]
|
||||
ReferralAct ReferralAct[]
|
||||
Streak Streak[]
|
||||
Subscriptions Subscription[]
|
||||
ThreadSubscriptions ThreadSubscription[]
|
||||
Upload Upload[] @relation("Uploads")
|
||||
nostrRelays UserNostrRelay[]
|
||||
|
@ -102,6 +101,7 @@ model User {
|
|||
ArcIn Arc[] @relation("toUser")
|
||||
Sub Sub[]
|
||||
SubAct SubAct[]
|
||||
MuteSub MuteSub[]
|
||||
|
||||
@@index([photoId])
|
||||
@@index([createdAt], map: "users.created_at_index")
|
||||
|
@ -419,12 +419,12 @@ model Sub {
|
|||
moderated Boolean @default(false)
|
||||
moderatedCount Int @default(0)
|
||||
|
||||
parent Sub? @relation("ParentChildren", fields: [parentName], references: [name])
|
||||
children Sub[] @relation("ParentChildren")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
Item Item[]
|
||||
Subscription Subscription[]
|
||||
SubAct SubAct[]
|
||||
parent Sub? @relation("ParentChildren", fields: [parentName], references: [name])
|
||||
children Sub[] @relation("ParentChildren")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
Item Item[]
|
||||
SubAct SubAct[]
|
||||
MuteSub MuteSub[]
|
||||
|
||||
@@index([parentName])
|
||||
@@index([createdAt])
|
||||
|
@ -451,14 +451,17 @@ model SubAct {
|
|||
@@index([userId, createdAt, type])
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id Int @id @default(autoincrement())
|
||||
model MuteSub {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
subName String @db.Citext
|
||||
userId Int
|
||||
sub Sub @relation(fields: [subName], references: [name], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([userId, subName])
|
||||
@@index([subName])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
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 {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue