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,7 +60,54 @@ export default function Share ({ item }) {
|
|||||||
>
|
>
|
||||||
copy link
|
copy link
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
{mine && !item?.noteId && (
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CopyLinkDropdownItem ({ item }) {
|
||||||
|
const me = useMe()
|
||||||
|
const toaster = useToast()
|
||||||
|
const url = referrurl(`/items/${item.id}`, me)
|
||||||
|
return (
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (navigator.share) {
|
||||||
|
await navigator.share({
|
||||||
|
title: item.title || '',
|
||||||
|
text: '',
|
||||||
|
url
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await copy(url)
|
||||||
|
}
|
||||||
|
toaster.success('link copied')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger('failed to copy link')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
copy link
|
||||||
|
</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
|
<Dropdown.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
@ -110,36 +143,5 @@ export default function Share ({ item }) {
|
|||||||
>
|
>
|
||||||
crosspost to nostr
|
crosspost to nostr
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
)}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CopyLinkDropdownItem ({ item }) {
|
|
||||||
const me = useMe()
|
|
||||||
const toaster = useToast()
|
|
||||||
const url = getShareUrl(item, me)
|
|
||||||
return (
|
|
||||||
<Dropdown.Item
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
if (navigator.share) {
|
|
||||||
await navigator.share({
|
|
||||||
title: item.title || '',
|
|
||||||
text: '',
|
|
||||||
url
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await copy(url)
|
|
||||||
}
|
|
||||||
toaster.success('link copied')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toaster.danger('failed to copy link')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
copy link
|
|
||||||
</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")
|
||||||
@ -423,8 +423,8 @@ model Sub {
|
|||||||
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