Add nsfw setting to territories (#788)
* add nsfw column to sub * add nsfw boolean to territorySchema * save nsfw value in upsertSub mutation * return nsfw value from Sub query for correct value in edit territory form * add nsfw checkbox to territory form * add nsfw badge to territory header * add nsfwMode to user * show nsfw badge next to item territory * exclude nsfw sub from items query * show nsfw mode checkbox on settings page * fix nsfw badge formatting * separate user from current, signed in user * update relationClause to join with sub table * refactor to simplify hide nsfw sql * filter nsfw items when viewing user items * hide nsfw posts for logged out users * filter nsfw subs based on user preference * show nsfw sub name if logged out user is viewing the page * show current sub at the top of the list instead of bottom * always join item with sub to check nsfw * check for sub presence before showing nsfw badge on item * skip manually adding sub to select if sub is null * fix relationClause to join with root item * move moderation and nsfw into accordion --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
parent
b3d485e8c4
commit
6355d7eabc
@ -152,19 +152,19 @@ const relationClause = (type) => {
|
||||
let clause = ''
|
||||
switch (type) {
|
||||
case 'comments':
|
||||
clause += ' FROM "Item" JOIN "Item" root ON "Item"."rootId" = root.id '
|
||||
clause += ' FROM "Item" JOIN "Item" root ON "Item"."rootId" = root.id LEFT JOIN "Sub" ON "Sub"."name" = root."subName" '
|
||||
break
|
||||
case 'bookmarks':
|
||||
clause += ' FROM "Item" JOIN "Bookmark" ON "Bookmark"."itemId" = "Item"."id" '
|
||||
clause += ' FROM "Item" JOIN "Bookmark" ON "Bookmark"."itemId" = "Item"."id" LEFT JOIN "Sub" ON "Sub"."name" = "Item"."subName" '
|
||||
break
|
||||
case 'outlawed':
|
||||
case 'borderland':
|
||||
case 'freebies':
|
||||
case 'all':
|
||||
clause += ' FROM "Item" LEFT JOIN "Item" root ON "Item"."rootId" = root.id '
|
||||
clause += ' FROM "Item" LEFT JOIN "Item" root ON "Item"."rootId" = root.id LEFT JOIN "Sub" ON "Sub"."name" = root."subName" '
|
||||
break
|
||||
default:
|
||||
clause += ' FROM "Item" '
|
||||
clause += ' FROM "Item" LEFT JOIN "Sub" ON "Sub"."name" = "Item"."subName" '
|
||||
}
|
||||
|
||||
return clause
|
||||
@ -192,12 +192,20 @@ 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, me) => {
|
||||
return sub
|
||||
? `${table ? `"${table}".` : ''}"subName" = $${num}::CITEXT`
|
||||
: me
|
||||
? `NOT EXISTS (SELECT 1 FROM "MuteSub" WHERE "MuteSub"."userId" = ${me.id} AND "MuteSub"."subName" = ${table ? `"${table}".` : ''}"subName")`
|
||||
: ''
|
||||
const HIDE_NSFW_CLAUSE = '"Sub"."nsfw" = FALSE'
|
||||
|
||||
export const nsfwClause = showNsfw => showNsfw ? '' : HIDE_NSFW_CLAUSE
|
||||
|
||||
const subClause = (sub, num, table, me, showNsfw) => {
|
||||
// Intentionally show nsfw posts (i.e. no nsfw clause) when viewing a specific nsfw sub
|
||||
if (sub) { return `${table ? `"${table}".` : ''}"subName" = $${num}::CITEXT` }
|
||||
|
||||
if (!me) { return HIDE_NSFW_CLAUSE }
|
||||
|
||||
const excludeMuted = `NOT EXISTS (SELECT 1 FROM "MuteSub" WHERE "MuteSub"."userId" = ${me.id} AND "MuteSub"."subName" = ${table ? `"${table}".` : ''}"subName")`
|
||||
if (showNsfw) return excludeMuted
|
||||
|
||||
return excludeMuted + ' AND ' + HIDE_NSFW_CLAUSE
|
||||
}
|
||||
|
||||
export async function filterClause (me, models, type) {
|
||||
@ -307,6 +315,9 @@ export default {
|
||||
// but the query planner doesn't like unused parameters
|
||||
const subArr = sub ? [sub] : []
|
||||
|
||||
const currentUser = me ? await models.user.findUnique({ where: { id: me.id } }) : null
|
||||
const showNsfw = currentUser ? currentUser.nsfwMode : false
|
||||
|
||||
switch (sort) {
|
||||
case 'user':
|
||||
if (!name) {
|
||||
@ -329,6 +340,7 @@ export default {
|
||||
`"${table}"."userId" = $3`,
|
||||
activeOrMine(me),
|
||||
await filterClause(me, models, type),
|
||||
nsfwClause(showNsfw),
|
||||
typeClause(type),
|
||||
whenClause(when || 'forever', table))}
|
||||
${orderByClause(by, me, models, type)}
|
||||
@ -346,7 +358,7 @@ export default {
|
||||
${relationClause(type)}
|
||||
${whereClause(
|
||||
'"Item".created_at <= $1',
|
||||
subClause(sub, 4, subClauseTable(type), me),
|
||||
subClause(sub, 4, subClauseTable(type), me, showNsfw),
|
||||
activeOrMine(me),
|
||||
await filterClause(me, models, type),
|
||||
typeClause(type),
|
||||
@ -370,7 +382,7 @@ export default {
|
||||
${joinZapRankPersonalView(me, models)}
|
||||
${whereClause(
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
subClause(sub, 5, subClauseTable(type), me),
|
||||
subClause(sub, 5, subClauseTable(type), me, showNsfw),
|
||||
typeClause(type),
|
||||
whenClause(when, 'Item'),
|
||||
await filterClause(me, models, type),
|
||||
@ -389,7 +401,7 @@ export default {
|
||||
${relationClause(type)}
|
||||
${whereClause(
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
subClause(sub, 5, subClauseTable(type), me),
|
||||
subClause(sub, 5, subClauseTable(type), me, showNsfw),
|
||||
typeClause(type),
|
||||
whenClause(when, 'Item'),
|
||||
await filterClause(me, models, type),
|
||||
@ -440,13 +452,14 @@ export default {
|
||||
query: `
|
||||
${SELECT}, ${me ? 'GREATEST(g.tf_hot_score, l.tf_hot_score)' : 'g.tf_hot_score'} AS rank
|
||||
FROM "Item"
|
||||
LEFT JOIN "Sub" ON "Sub"."name" = "Item"."subName"
|
||||
${joinZapRankPersonalView(me, models)}
|
||||
${whereClause(
|
||||
'"Item"."pinId" IS NULL',
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
'"Item"."parentId" IS NULL',
|
||||
'"Item".bio = false',
|
||||
subClause(sub, 3, 'Item', me),
|
||||
subClause(sub, 3, 'Item', me, showNsfw),
|
||||
muteClause(me))}
|
||||
ORDER BY rank DESC
|
||||
OFFSET $1
|
||||
@ -462,8 +475,9 @@ export default {
|
||||
query: `
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
LEFT JOIN "Sub" ON "Sub"."name" = "Item"."subName"
|
||||
${whereClause(
|
||||
subClause(sub, 3, 'Item', me),
|
||||
subClause(sub, 3, 'Item', me, showNsfw),
|
||||
muteClause(me),
|
||||
// in "home" (sub undefined), we want to show pinned items (but without the pin icon)
|
||||
sub ? '"Item"."pinId" IS NULL' : '',
|
||||
|
@ -84,21 +84,24 @@ export default {
|
||||
sub: getSub,
|
||||
subs: async (parent, args, { models, me }) => {
|
||||
if (me) {
|
||||
return await models.$queryRaw`
|
||||
const currentUser = await models.user.findUnique({ where: { id: me.id } })
|
||||
const showNsfw = currentUser ? currentUser.nsfwMode : false
|
||||
return await models.$queryRawUnsafe(`
|
||||
SELECT "Sub".*, COALESCE(json_agg("MuteSub".*) FILTER (WHERE "MuteSub"."userId" IS NOT NULL), '[]') AS "MuteSub"
|
||||
FROM "Sub"
|
||||
LEFT JOIN "MuteSub" ON "Sub".name = "MuteSub"."subName" AND "MuteSub"."userId" = ${me.id}::INTEGER
|
||||
WHERE status <> 'STOPPED'
|
||||
WHERE status <> 'STOPPED' ${showNsfw ? '' : 'AND "Sub"."nsfw" = FALSE'}
|
||||
GROUP BY "Sub".name, "MuteSub"."userId"
|
||||
ORDER BY "Sub".name ASC
|
||||
`
|
||||
`)
|
||||
}
|
||||
|
||||
return await models.sub.findMany({
|
||||
where: {
|
||||
status: {
|
||||
not: 'STOPPED'
|
||||
}
|
||||
},
|
||||
nsfw: false
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
@ -193,7 +196,7 @@ export default {
|
||||
}
|
||||
|
||||
async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
|
||||
const { billingType } = data
|
||||
const { billingType, nsfw } = data
|
||||
let billingCost = TERRITORY_COST_MONTHLY
|
||||
let billAt = datePivot(new Date(), { months: 1 })
|
||||
|
||||
@ -226,7 +229,8 @@ async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
|
||||
...data,
|
||||
billingCost,
|
||||
rankingType: 'WOT',
|
||||
userId: me.id
|
||||
userId: me.id,
|
||||
nsfw
|
||||
}
|
||||
}),
|
||||
// record 'em
|
||||
|
@ -11,7 +11,7 @@ export default gql`
|
||||
upsertSub(oldName: String, name: String!, desc: String, baseCost: Int!,
|
||||
postTypes: [String!]!, allowFreebies: Boolean!,
|
||||
billingType: String!, billingAutoRenew: Boolean!,
|
||||
moderated: Boolean!, hash: String, hmac: String): Sub
|
||||
moderated: Boolean!, hash: String, hmac: String, nsfw: Boolean!): Sub
|
||||
paySub(name: String!, hash: String, hmac: String): Sub
|
||||
toggleMuteSub(name: String!): Boolean!
|
||||
}
|
||||
@ -35,5 +35,6 @@ export default gql`
|
||||
moderated: Boolean!
|
||||
moderatedCount: Int!
|
||||
meMuteSub: Boolean!
|
||||
nsfw: Boolean!
|
||||
}
|
||||
`
|
||||
|
@ -80,6 +80,7 @@ export default gql`
|
||||
noteItemSats: Boolean!
|
||||
noteJobIndicator: Boolean!
|
||||
noteMentions: Boolean!
|
||||
nsfwMode: Boolean!
|
||||
tipDefault: Int!
|
||||
turboTipping: Boolean!
|
||||
wildWestMode: Boolean!
|
||||
@ -138,6 +139,7 @@ export default gql`
|
||||
noteItemSats: Boolean!
|
||||
noteJobIndicator: Boolean!
|
||||
noteMentions: Boolean!
|
||||
nsfwMode: Boolean!
|
||||
tipDefault: Int!
|
||||
turboTipping: Boolean!
|
||||
wildWestMode: Boolean!
|
||||
|
@ -122,6 +122,8 @@ export default function ItemInfo ({
|
||||
<Link href={`/~${item.subName}`}>
|
||||
{' '}<Badge className={styles.newComment} bg={null}>{item.subName}</Badge>
|
||||
</Link>}
|
||||
{sub?.nsfw &&
|
||||
<Badge className={styles.newComment} bg={null}>nsfw</Badge>}
|
||||
{(item.outlawed && !item.mine &&
|
||||
<Link href='/recent/outlawed'>
|
||||
{' '}<Badge className={styles.newComment} bg={null}>outlawed</Badge>
|
||||
|
@ -53,6 +53,10 @@ export default function SubSelect ({ prependSubs, sub, onChange, large, appendSu
|
||||
overrideValue: sub
|
||||
}
|
||||
|
||||
// If logged out user directly visits a nsfw sub, subs will not contain `sub`, so manually add it
|
||||
// to display the correct sub name in the sub selector
|
||||
const subItems = !sub || subs.find((s) => s === sub) ? subs : [sub].concat(subs)
|
||||
|
||||
return (
|
||||
<Select
|
||||
onChange={onChange || ((_, e) => {
|
||||
@ -102,7 +106,7 @@ export default function SubSelect ({ prependSubs, sub, onChange, large, appendSu
|
||||
{...valueProps}
|
||||
{...props}
|
||||
className={`${className} ${styles.subSelect} ${large ? 'me-2' : styles.subSelectSmall}`}
|
||||
items={subs}
|
||||
items={subItems}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import AccordianItem from './accordian-item'
|
||||
import { Col, InputGroup, Row, Form as BootstrapForm, Badge } from 'react-bootstrap'
|
||||
import { Checkbox, CheckboxGroup, Form, Input, MarkdownInput } from './form'
|
||||
import FeeButton, { FeeButtonProvider } from './fee-button'
|
||||
@ -17,10 +18,10 @@ export default function TerritoryForm ({ sub }) {
|
||||
gql`
|
||||
mutation upsertSub($oldName: String, $name: String!, $desc: String, $baseCost: Int!,
|
||||
$postTypes: [String!]!, $allowFreebies: Boolean!, $billingType: String!,
|
||||
$billingAutoRenew: Boolean!, $moderated: Boolean!, $hash: String, $hmac: String) {
|
||||
$billingAutoRenew: Boolean!, $moderated: Boolean!, $hash: String, $hmac: String, $nsfw: Boolean!) {
|
||||
upsertSub(oldName: $oldName, name: $name, desc: $desc, baseCost: $baseCost,
|
||||
postTypes: $postTypes, allowFreebies: $allowFreebies, billingType: $billingType,
|
||||
billingAutoRenew: $billingAutoRenew, moderated: $moderated, hash: $hash, hmac: $hmac) {
|
||||
billingAutoRenew: $billingAutoRenew, moderated: $moderated, hash: $hash, hmac: $hmac, nsfw: $nsfw) {
|
||||
name
|
||||
}
|
||||
}`
|
||||
@ -65,7 +66,8 @@ export default function TerritoryForm ({ sub }) {
|
||||
allowFreebies: typeof sub?.allowFreebies === 'undefined' ? true : sub?.allowFreebies,
|
||||
billingType: sub?.billingType || 'MONTHLY',
|
||||
billingAutoRenew: sub?.billingAutoRenew || false,
|
||||
moderated: sub?.moderated || false
|
||||
moderated: sub?.moderated || false,
|
||||
nsfw: sub?.nsfw || false
|
||||
}}
|
||||
schema={territorySchema({ client, me })}
|
||||
invoiceable
|
||||
@ -145,22 +147,6 @@ export default function TerritoryForm ({ sub }) {
|
||||
</Col>
|
||||
</Row>
|
||||
</CheckboxGroup>
|
||||
<BootstrapForm.Label>moderation</BootstrapForm.Label>
|
||||
<Checkbox
|
||||
inline
|
||||
label={
|
||||
<div className='d-flex align-items-center'>enable moderation
|
||||
<Info>
|
||||
<ol>
|
||||
<li>Outlaw posts and comments with a click</li>
|
||||
<li>Your territory will get a <Badge bg='secondary'>moderated</Badge> badge</li>
|
||||
</ol>
|
||||
</Info>
|
||||
</div>
|
||||
}
|
||||
name='moderated'
|
||||
groupClassName='ms-1'
|
||||
/>
|
||||
<CheckboxGroup
|
||||
label='billing'
|
||||
name='billing'
|
||||
@ -206,6 +192,46 @@ export default function TerritoryForm ({ sub }) {
|
||||
name='billingAutoRenew'
|
||||
groupClassName='ms-1 mt-2'
|
||||
/>}
|
||||
<AccordianItem
|
||||
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>}
|
||||
body={
|
||||
<>
|
||||
<BootstrapForm.Label>moderation</BootstrapForm.Label>
|
||||
<Checkbox
|
||||
inline
|
||||
label={
|
||||
<div className='d-flex align-items-center'>enable moderation
|
||||
<Info>
|
||||
<ol>
|
||||
<li>Outlaw posts and comments with a click</li>
|
||||
<li>Your territory will get a <Badge bg='secondary'>moderated</Badge> badge</li>
|
||||
</ol>
|
||||
</Info>
|
||||
</div>
|
||||
}
|
||||
name='moderated'
|
||||
groupClassName='ms-1'
|
||||
/>
|
||||
<BootstrapForm.Label>nsfw</BootstrapForm.Label>
|
||||
<Checkbox
|
||||
inline
|
||||
label={
|
||||
<div className='d-flex align-items-center'>mark as nsfw
|
||||
<Info>
|
||||
<ol>
|
||||
<li>Let stackers know that your territory may contain explicit content</li>
|
||||
<li>Your territory will get a <Badge bg='secondary'>nsfw</Badge> badge</li>
|
||||
</ol>
|
||||
</Info>
|
||||
</div>
|
||||
}
|
||||
name='nsfw'
|
||||
groupClassName='ms-1'
|
||||
/>
|
||||
</>
|
||||
|
||||
}
|
||||
/>
|
||||
<div className='mt-3 d-flex justify-content-end'>
|
||||
<FeeButton
|
||||
text={sub ? 'save' : 'found it'}
|
||||
|
@ -19,6 +19,7 @@ export function TerritoryDetails ({ sub }) {
|
||||
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 > 0 && ` ${sub.moderatedCount}`}</Badge>}
|
||||
{(sub.nsfw) && <Badge className='ms-2' bg='secondary'>nsfw</Badge>}
|
||||
</small>
|
||||
}
|
||||
>
|
||||
|
@ -22,6 +22,7 @@ export const ITEM_FIELDS = gql`
|
||||
userId
|
||||
moderated
|
||||
meMuteSub
|
||||
nsfw
|
||||
}
|
||||
otsHash
|
||||
position
|
||||
|
@ -19,6 +19,7 @@ export const SUB_FIELDS = gql`
|
||||
moderated
|
||||
moderatedCount
|
||||
meMuteSub
|
||||
nsfw
|
||||
}`
|
||||
|
||||
export const SUB_FULL_FIELDS = gql`
|
||||
|
@ -82,6 +82,7 @@ export const SETTINGS_FIELDS = gql`
|
||||
nostrRelays
|
||||
wildWestMode
|
||||
greeterMode
|
||||
nsfwMode
|
||||
authMethods {
|
||||
lightning
|
||||
nostr
|
||||
|
@ -304,7 +304,8 @@ export function territorySchema (args) {
|
||||
.min(1, 'must be at least 1')
|
||||
.max(100000, 'must be at most 100k'),
|
||||
postTypes: array().of(string().oneOf(POST_TYPES)).min(1, 'must support at least one post type'),
|
||||
billingType: string().required('required').oneOf(TERRITORY_BILLING_TYPES, 'required')
|
||||
billingType: string().required('required').oneOf(TERRITORY_BILLING_TYPES, 'required'),
|
||||
nsfw: boolean()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -82,6 +82,7 @@ export default function Settings ({ ssrData }) {
|
||||
imgproxyOnly: settings?.imgproxyOnly,
|
||||
wildWestMode: settings?.wildWestMode,
|
||||
greeterMode: settings?.greeterMode,
|
||||
nsfwMode: settings?.nsfwMode,
|
||||
nostrPubkey: settings?.nostrPubkey ? bech32encode(settings.nostrPubkey) : '',
|
||||
nostrCrossposting: settings?.nostrCrossposting,
|
||||
nostrRelays: settings?.nostrRelays?.length ? settings?.nostrRelays : [''],
|
||||
@ -354,6 +355,19 @@ export default function Settings ({ ssrData }) {
|
||||
</div>
|
||||
}
|
||||
name='greeterMode'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label={
|
||||
<div className='d-flex align-items-center'>nsfw mode
|
||||
<Info>
|
||||
<ul className='fw-bold'>
|
||||
<li>see posts from nsfw territories</li>
|
||||
</ul>
|
||||
</Info>
|
||||
</div>
|
||||
}
|
||||
name='nsfwMode'
|
||||
/>
|
||||
<h4>nostr</h4>
|
||||
<Checkbox
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Sub" ADD COLUMN "nsfw" BOOLEAN NOT NULL DEFAULT false;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "nsfwMode" BOOLEAN NOT NULL DEFAULT false;
|
@ -49,6 +49,7 @@ model User {
|
||||
hideInvoiceDesc Boolean @default(false)
|
||||
wildWestMode Boolean @default(false)
|
||||
greeterMode Boolean @default(false)
|
||||
nsfwMode Boolean @default(false)
|
||||
fiatCurrency String @default("USD")
|
||||
withdrawMaxFeeDefault Int @default(10)
|
||||
autoDropBolt11s Boolean @default(false)
|
||||
@ -423,6 +424,7 @@ model Sub {
|
||||
billedLastAt DateTime @default(now())
|
||||
moderated Boolean @default(false)
|
||||
moderatedCount Int @default(0)
|
||||
nsfw Boolean @default(false)
|
||||
|
||||
parent Sub? @relation("ParentChildren", fields: [parentName], references: [name])
|
||||
children Sub[] @relation("ParentChildren")
|
||||
|
Loading…
x
Reference in New Issue
Block a user