stacker.news/components/territory-header.js

228 lines
6.9 KiB
JavaScript
Raw Normal View History

2023-12-31 01:41:16 +00:00
import { Badge, Button, CardFooter, Dropdown } from 'react-bootstrap'
2023-12-15 18:10:29 +00:00
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'
2023-12-15 18:10:29 +00:00
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'
import ActionDropdown from './action-dropdown'
Territory transfers (#878) * Allow founders to transfer territories * Log territory transfers in new AuditLog table * Add territory transfer notifications * Use polymorphic AuditEvent table * Add setting for territory transfer notifications * Add push notification * Rename label from user to stacker * More space between cancel and confirm button * Remove AuditEvent table The audit table is not necessary for territory transfers and only adds complexity and unrelated discussion to this PR. Thinking about a future-proof schema for territory transfers and how/what to audit at the same time made my head spin. Some thoughts I had: 1. Maybe using polymorphism for an audit log / audit events is not a good idea Using polymorphism as is currently used in the code base (user wallets) means that every generic event must map to exactly one specialized event. Is this a good requirement/assumption? It already didn't work well for naive auditing of territory transfers since we want events to be indexable by user (no array column) so every event needs to point to a single user but a territory transfer involves multiple users. This made me wonder: Do we even need a table? Maybe the audit log for a user can be implemented using a view? This would also mean no data denormalization. 2. What to audit and how and why? Most actions are already tracked in some way by necessity: zaps, items, mutes, payments, ... In that case: what is the benefit of tracking these things individually in a separate table? Denormalize simply for convenience or performance? Why no view (see previous point)? Use case needs to be more clearly defined before speccing out a schema. * Fix territory transfer notification id conflict * Use include instead of two separate queries * Drop territory transfer setting * Remove trigger usage * Prevent transfers to yourself
2024-03-05 19:56:02 +00:00
import { TerritoryTransferDropdownItem } from './territory-transfer'
2023-12-15 18:10:29 +00:00
2024-02-23 15:32:20 +00:00
export function TerritoryDetails ({ sub, children }) {
return (
<AccordianCard
header={
<small className='text-muted fw-bold align-items-center d-flex'>
2024-02-23 15:32:20 +00:00
{sub.name}
{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 > 0 && ` ${sub.moderatedCount}`}</Badge>}
{(sub.nsfw) && <Badge className='ms-2' bg='secondary'>nsfw</Badge>}
</small>
}
>
2024-02-23 15:32:20 +00:00
{children}
2024-01-09 01:02:00 +00:00
<TerritoryInfo sub={sub} />
</AccordianCard>
)
}
export function TerritoryInfo ({ sub }) {
return (
<>
<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>
<span> on </span>
2024-04-02 23:17:32 +00:00
<span className='fw-bold'>{new Date(sub.createdAt).toDateString()}</span>
</div>
<div className='text-muted'>
<span>post cost </span>
<span className='fw-bold'>{numWithUnits(sub.baseCost)}</span>
</div>
<TerritoryBillingLine sub={sub} />
</CardFooter>
2024-01-09 01:02:00 +00:00
</>
)
}
2023-12-15 18:10:29 +00:00
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: {
2023-12-31 01:41:16 +00:00
meMuteSub: () => toggleMuteSub
2023-12-15 18:10:29 +00:00
}
})
}
}
)
Territory transfers (#878) * Allow founders to transfer territories * Log territory transfers in new AuditLog table * Add territory transfer notifications * Use polymorphic AuditEvent table * Add setting for territory transfer notifications * Add push notification * Rename label from user to stacker * More space between cancel and confirm button * Remove AuditEvent table The audit table is not necessary for territory transfers and only adds complexity and unrelated discussion to this PR. Thinking about a future-proof schema for territory transfers and how/what to audit at the same time made my head spin. Some thoughts I had: 1. Maybe using polymorphism for an audit log / audit events is not a good idea Using polymorphism as is currently used in the code base (user wallets) means that every generic event must map to exactly one specialized event. Is this a good requirement/assumption? It already didn't work well for naive auditing of territory transfers since we want events to be indexable by user (no array column) so every event needs to point to a single user but a territory transfer involves multiple users. This made me wonder: Do we even need a table? Maybe the audit log for a user can be implemented using a view? This would also mean no data denormalization. 2. What to audit and how and why? Most actions are already tracked in some way by necessity: zaps, items, mutes, payments, ... In that case: what is the benefit of tracking these things individually in a separate table? Denormalize simply for convenience or performance? Why no view (see previous point)? Use case needs to be more clearly defined before speccing out a schema. * Fix territory transfer notification id conflict * Use include instead of two separate queries * Drop territory transfer setting * Remove trigger usage * Prevent transfers to yourself
2024-03-05 19:56:02 +00:00
const isMine = Number(sub.userId) === Number(me?.id)
2023-12-15 18:10:29 +00:00
return (
<>
<TerritoryPaymentDue sub={sub} />
2024-07-11 21:58:55 +00:00
<div className='mb-2 mt-1'>
2023-12-15 18:10:29 +00:00
<div>
2024-02-23 15:32:20 +00:00
<TerritoryDetails sub={sub}>
<div className='d-flex my-2 justify-content-end'>
{sub.name}
<Share path={`/~${sub.name}`} title={`~${sub.name} stacker news territory`} className='mx-1' />
{me &&
<>
{(isMine
? (
<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>)
)}
2024-02-23 15:32:20 +00:00
<ActionDropdown>
<ToggleSubSubscriptionDropdownItem sub={sub} />
{isMine && (
<>
<Dropdown.Divider />
<TerritoryTransferDropdownItem sub={sub} />
</>
)}
</ActionDropdown>
</>}
</div>
</TerritoryDetails>
2023-12-15 18:10:29 +00:00
</div>
</div>
</>
)
}
2023-12-31 01:41:16 +00:00
export function MuteSubDropdownItem ({ item, sub }) {
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
}
})
}
}
)
return (
<Dropdown.Item
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 ? 'unmute' : 'mute'} ~{sub.name}
</Dropdown.Item>
)
}
export function PinSubDropdownItem ({ item: { id, position } }) {
const toaster = useToast()
const [pinItem] = useMutation(
gql`
mutation pinItem($id: ID!) {
pinItem(id: $id) {
position
}
}`, {
// refetch since position of other items might also have changed to fill gaps
refetchQueries: ['SubItems', 'Item']
}
)
return (
<Dropdown.Item
onClick={async () => {
try {
await pinItem({ variables: { id } })
toaster.success(position ? 'pin removed' : 'pin added')
} catch (err) {
toaster.danger(err.message)
}
}}
>
{position ? 'unpin item' : 'pin item'}
</Dropdown.Item>
)
}
export function ToggleSubSubscriptionDropdownItem ({ sub: { name, meSubscription } }) {
const toaster = useToast()
const [toggleSubSubscription] = useMutation(
gql`
mutation toggleSubSubscription($name: String!) {
toggleSubSubscription(name: $name)
}`, {
update (cache, { data: { toggleSubSubscription } }) {
cache.modify({
id: `Sub:{"name":"${name}"}`,
fields: {
meSubscription: () => toggleSubSubscription
}
})
}
}
)
return (
<Dropdown.Item
onClick={async () => {
try {
await toggleSubSubscription({ variables: { name } })
toaster.success(meSubscription ? 'unsubscribed' : 'subscribed')
} catch (err) {
console.error(err)
toaster.danger(meSubscription ? 'failed to unsubscribe' : 'failed to subscribe')
}
}}
>
{meSubscription ? `unsubscribe from ~${name}` : `subscribe to ~${name}`}
</Dropdown.Item>
)
}