Skip to content

Commit

Permalink
opt-in moderation for territory founders
Browse files Browse the repository at this point in the history
  • Loading branch information
huumn committed Dec 30, 2023
1 parent 7f512d6 commit dc15be9
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 23 deletions.
71 changes: 64 additions & 7 deletions api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,20 @@ export function joinZapRankPersonalView (me, models) {
async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args) {
if (!me) {
return await models.$queryRawUnsafe(`
SELECT "Item".*, to_json(users.*) as user
SELECT "Item".*, to_json(users.*) as user, to_jsonb("Sub".*) as sub
FROM (
${query}
) "Item"
JOIN users ON "Item"."userId" = users.id
LEFT JOIN "Sub" ON "Sub"."name" = "Item"."subName"
${orderBy}`, ...args)
} else {
return await models.$queryRawUnsafe(`
SELECT "Item".*, to_jsonb(users.*) || jsonb_build_object('meMute', "Mute"."mutedId" IS NOT NULL) as user,
COALESCE("ItemAct"."meMsats", 0) as "meMsats",
COALESCE("ItemAct"."meDontLikeMsats", 0) as "meDontLikeMsats", b."itemId" IS NOT NULL AS "meBookmark",
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward"
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward",
to_jsonb("Sub".*) as sub
FROM (
${query}
) "Item"
Expand All @@ -132,6 +134,7 @@ async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args)
LEFT JOIN "Bookmark" b ON b."itemId" = "Item".id AND b."userId" = ${me.id}
LEFT JOIN "ThreadSubscription" ON "ThreadSubscription"."itemId" = "Item".id AND "ThreadSubscription"."userId" = ${me.id}
LEFT JOIN "ItemForward" ON "ItemForward"."itemId" = "Item".id AND "ItemForward"."userId" = ${me.id}
LEFT JOIN "Sub" ON "Sub"."name" = "Item"."subName"
LEFT JOIN LATERAL (
SELECT "itemId", sum("ItemAct".msats) FILTER (WHERE act = 'FEE' OR act = 'TIP') AS "meMsats",
sum("ItemAct".msats) FILTER (WHERE act = 'DONT_LIKE_THIS') AS "meDontLikeMsats"
Expand Down Expand Up @@ -224,7 +227,7 @@ export async function filterClause (me, models, type) {

// handle outlawed
// if the item is above the threshold or is mine
const outlawClauses = [`"Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`]
const outlawClauses = [`"Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD} AND NOT "Item".outlawed`]
if (me) {
outlawClauses.push(`"Item"."userId" = ${me.id}`)
}
Expand All @@ -250,7 +253,7 @@ function typeClause (type) {
case 'freebies':
return '"Item".freebie'
case 'outlawed':
return `"Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD}`
return `"Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD} OR "Item".outlawed`
case 'borderland':
return '"Item"."weightedVotes" - "Item"."weightedDownVotes" < 0'
case 'all':
Expand Down Expand Up @@ -787,6 +790,57 @@ export default {
act,
path: item.path
}
},
toggleOutlaw: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
}

const item = await models.item.findUnique({
where: { id: Number(id) },
include: {
sub: true,
root: {
include: {
sub: true
}
}
}
})

const sub = item.sub || item.root?.sub

if (Number(sub.userId) !== Number(me.id)) {
throw new GraphQLError('you cant do this broh', { extensions: { code: 'FORBIDDEN' } })
}

if (item.outlawed) {
return item
}

const [result] = await models.$transaction(
[
models.item.update({
where: {
id: Number(id)
},
data: {
outlawed: true
}
}),
models.sub.update({
where: {
name: sub.name
},
data: {
moderatedCount: {
increment: 1
}
}
})
])

return result
}
},
Item: {
Expand Down Expand Up @@ -967,7 +1021,7 @@ export default {
if (me && Number(item.userId) === Number(me.id)) {
return false
}
return item.weightedVotes - item.weightedDownVotes <= -ITEM_FILTER_THRESHOLD
return item.outlawed || item.weightedVotes - item.weightedDownVotes <= -ITEM_FILTER_THRESHOLD
},
mine: async (item, args, { me, models }) => {
return me?.id === item.userId
Expand All @@ -979,7 +1033,10 @@ export default {
if (item.root) {
return item.root
}
return await models.item.findUnique({ where: { id: item.rootId } })
return await models.item.findUnique({
where: { id: item.rootId },
include: { sub: true }
})
},
parent: async (item, args, { models }) => {
if (!item.parentId) {
Expand Down Expand Up @@ -1207,7 +1264,7 @@ export const SELECT =
"Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", "Item".boost, "Item".msats,
"Item".ncomments, "Item"."commentMsats", "Item"."lastCommentAt", "Item"."weightedVotes",
"Item"."weightedDownVotes", "Item".freebie, "Item".bio, "Item"."otsHash", "Item"."bountyPaidTo",
ltree2text("Item"."path") AS "path", "Item"."weightedComments", "Item"."imgproxyUrls"`
ltree2text("Item"."path") AS "path", "Item"."weightedComments", "Item"."imgproxyUrls", "Item".outlawed`

function topOrderByWeightedSats (me, models) {
return `ORDER BY ${orderByNumerator(models)} DESC NULLS LAST, "Item".id DESC`
Expand Down
2 changes: 1 addition & 1 deletion api/resolvers/sub.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export default {
await ssValidate(territorySchema, data, { models, me })

if (old) {
return await updateSub(parent, data, { me, models, lnd, hash, hmac, old })
return await updateSub(parent, data, { me, models, lnd, hash, hmac })
} else {
return await createSub(parent, data, { me, models, lnd, hash, hmac })
}
Expand Down
1 change: 1 addition & 0 deletions api/typeDefs/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default gql`
upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item!
act(id: ID!, sats: Int, act: String, idempotent: Boolean, hash: String, hmac: String): ItemActResult!
pollVote(id: ID!, hash: String, hmac: String): ID!
toggleOutlaw(id: ID!): Item!
}
type PollOption {
Expand Down
4 changes: 3 additions & 1 deletion api/typeDefs/sub.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default gql`
upsertSub(name: String!, desc: String, baseCost: Int!,
postTypes: [String!]!, allowFreebies: Boolean!,
billingType: String!, billingAutoRenew: Boolean!,
hash: String, hmac: String): Sub
moderated: Boolean!, hash: String, hmac: String): Sub
paySub(name: String!, hash: String, hmac: String): Sub
}
Expand All @@ -31,5 +31,7 @@ export default gql`
billedLastAt: Date!
baseCost: Int!
status: String!
moderated: Boolean!
moderatedCount: Int!
}
`
39 changes: 39 additions & 0 deletions components/dont-link-this.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AccordianItem from './accordian-item'
import Flag from '../svgs/flag-fill.svg'
import { useMemo } from 'react'
import getColor from '../lib/rainbow'
import { gql, useMutation } from '@apollo/client'

export function DownZap ({ id, meDontLikeSats, ...props }) {
const style = useMemo(() => (meDontLikeSats
Expand Down Expand Up @@ -64,3 +65,41 @@ export default function DontLikeThisDropdownItem ({ id }) {
</DownZapper>
)
}

export function OutlawDropdownItem ({ item }) {
const toaster = useToast()

const [toggleOutlaw] = useMutation(
gql`
mutation toggleOutlaw($id: ID!) {
toggleOutlaw(id: $id) {
outlawed
}
}`, {
update (cache, { data: { toggleOutlaw } }) {
cache.modify({
id: `Item:${item.id}`,
fields: {
outlawed: () => true
}
})
}
}
)

return (
<Dropdown.Item onClick={async () => {
try {
await toggleOutlaw({ variables: { id: item.id } })
} catch {
toaster.danger('failed to outlaw')
return
}

toaster.success('item outlawed')
}}
>
outlaw
</Dropdown.Item>
)
}
6 changes: 5 additions & 1 deletion components/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ export function Form ({
)
}

export function Select ({ label, items, groupClassName, onChange, noForm, overrideValue, ...props }) {
export function Select ({ label, items, groupClassName, onChange, noForm, overrideValue, hint, ...props }) {
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
const formik = noForm ? null : useFormikContext()
const invalid = meta.touched && meta.error
Expand Down Expand Up @@ -866,6 +866,10 @@ export function Select ({ label, items, groupClassName, onChange, noForm, overri
<BootstrapForm.Control.Feedback type='invalid'>
{meta.touched && meta.error}
</BootstrapForm.Control.Feedback>
{hint &&
<BootstrapForm.Text>
{hint}
</BootstrapForm.Text>}
</FormGroup>
)
}
Expand Down
20 changes: 14 additions & 6 deletions components/item-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { timeSince } from '../lib/time'
import { DeleteDropdownItem } from './delete'
import styles from './item.module.css'
import { useMe } from './me'
import DontLikeThisDropdownItem from './dont-link-this'
import DontLikeThisDropdownItem, { OutlawDropdownItem } from './dont-link-this'
import BookmarkDropdownItem from './bookmark'
import SubscribeDropdownItem from './subscribe'
import { CopyLinkDropdownItem } from './share'
Expand All @@ -19,6 +19,7 @@ import { AD_USER_ID } from '../lib/constants'
import ActionDropdown from './action-dropdown'
import MuteDropdownItem from './mute'
import { DropdownItemUpVote } from './upvote'
import { useRoot } from './root'

export default function ItemInfo ({
item, full, commentsText = 'comments',
Expand All @@ -32,6 +33,8 @@ export default function ItemInfo ({
useState(item.mine && (Date.now() < editThreshold))
const [hasNewComments, setHasNewComments] = useState(false)
const [meTotalSats, setMeTotalSats] = useState(0)
const root = useRoot()
const sub = item?.sub || root?.sub

useEffect(() => {
if (!full) {
Expand Down Expand Up @@ -146,18 +149,23 @@ export default function ItemInfo ({
<Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
opentimestamp
</Link>}
{me && !item.position &&
!item.mine && !item.deletedAt &&
(item.meDontLikeSats > meTotalSats
? <DropdownItemUpVote item={item} />
: <DontLikeThisDropdownItem id={item.id} />)}
{me && item?.noteId && (
<Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener')}>
nostr note
</Dropdown.Item>
)}
{me && !item.position &&
!item.mine && !item.deletedAt &&
(item.meDontLikeSats > meTotalSats
? <DropdownItemUpVote item={item} />
: <DontLikeThisDropdownItem id={item.id} />)}
{item.mine && !item.position && !item.deletedAt && !item.bio &&
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />}
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&
<>
<hr className='dropdown-divider' />
<OutlawDropdownItem item={item} />
</>}
{me && !item.mine &&
<>
<hr className='dropdown-divider' />
Expand Down
6 changes: 4 additions & 2 deletions components/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ export function PostForm ({ type, sub, children }) {
<Alert className='position-absolute' style={{ top: '-6rem' }} variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>
{errorMessage}
</Alert>}
<SubSelect prependSubs={['pick territory']} className='w-auto d-flex' noForm sub={sub?.name} />
<SubSelect prependSubs={['pick territory']} className='w-auto d-flex' noForm sub={sub?.name} hint={sub.moderated && 'this territory is moderated'} />

{postButtons}
<div className='d-flex mt-4'>
<AccordianItem
Expand Down Expand Up @@ -168,7 +169,8 @@ export default function Post ({ sub }) {
territory
<SubInfo />
</span>
}
}
hint={sub.moderated && 'this territory is moderated'}
/>}
</PostForm>
</>
Expand Down
4 changes: 4 additions & 0 deletions components/reply.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { toastDeleteScheduled } from '../lib/form'
import { ItemButtonBar } from './post'
import { useShowModal } from './modal'
import { Button } from 'react-bootstrap'
import { useRoot } from './root'

export function ReplyOnAnotherPage ({ item }) {
const path = item.path.split('.')
Expand All @@ -38,6 +39,8 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
const replyInput = useRef(null)
const toaster = useToast()
const showModal = useShowModal()
const root = useRoot()
const sub = item?.sub || root?.sub

useEffect(() => {
if (replyOpen || quote || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text')) {
Expand Down Expand Up @@ -174,6 +177,7 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
required
appendValue={quote}
placeholder={placeholder}
hint={sub?.moderated && 'this territory is moderated'}
/>
<ItemButtonBar createText='reply' hasCancel={false} />
</Form>
Expand Down
Loading

0 comments on commit dc15be9

Please sign in to comment.