From 6355d7eabc9516c478321082eb0442e2ee5d5f2b Mon Sep 17 00:00:00 2001 From: mzivil <158518982+mzivil@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:35:32 -0500 Subject: [PATCH] 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> --- api/resolvers/item.js | 44 ++++++++----- api/resolvers/sub.js | 16 +++-- api/typeDefs/sub.js | 3 +- api/typeDefs/user.js | 2 + components/item-info.js | 2 + components/sub-select.js | 6 +- components/territory-form.js | 64 +++++++++++++------ components/territory-header.js | 1 + fragments/items.js | 1 + fragments/subs.js | 1 + fragments/users.js | 1 + lib/validate.js | 3 +- pages/settings/index.js | 14 ++++ .../migration.sql | 2 + .../migration.sql | 2 + prisma/schema.prisma | 2 + 16 files changed, 121 insertions(+), 43 deletions(-) create mode 100644 prisma/migrations/20240205161535_add_nsfw_to_sub/migration.sql create mode 100644 prisma/migrations/20240207173333_add_nsfw_mode_to_user/migration.sql diff --git a/api/resolvers/item.js b/api/resolvers/item.js index dc6f6c37..5264a710 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -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' : '', diff --git a/api/resolvers/sub.js b/api/resolvers/sub.js index 35fe94c7..304a4cfe 100644 --- a/api/resolvers/sub.js +++ b/api/resolvers/sub.js @@ -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 diff --git a/api/typeDefs/sub.js b/api/typeDefs/sub.js index f15f9341..896d7278 100644 --- a/api/typeDefs/sub.js +++ b/api/typeDefs/sub.js @@ -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! } ` diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 4b0594c2..61b826e5 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -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! diff --git a/components/item-info.js b/components/item-info.js index 477a08da..bbfc0588 100644 --- a/components/item-info.js +++ b/components/item-info.js @@ -122,6 +122,8 @@ export default function ItemInfo ({ {' '}{item.subName} } + {sub?.nsfw && + nsfw} {(item.outlawed && !item.mine && {' '}outlawed diff --git a/components/sub-select.js b/components/sub-select.js index 2c4b079d..cb67702c 100644 --- a/components/sub-select.js +++ b/components/sub-select.js @@ -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 (