diff --git a/api/resolvers/item.js b/api/resolvers/item.js
index 8fff0491..8a02cf92 100644
--- a/api/resolvers/item.js
+++ b/api/resolvers/item.js
@@ -136,77 +136,6 @@ export async function joinSatRankView (me, models) {
return 'JOIN zap_rank_tender_view ON "Item".id = zap_rank_tender_view.id'
}
-export async function filterClause (me, models, type) {
- // if you are explicitly asking for marginal content, don't filter them
- if (['outlawed', 'borderland', 'freebies'].includes(type)) {
- if (me && ['outlawed', 'borderland'].includes(type)) {
- // unless the item is mine
- return ` AND "Item"."userId" <> ${me.id} `
- }
-
- return ''
- }
-
- // by default don't include freebies unless they have upvotes
- let clause = ' AND (NOT "Item".freebie OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0'
- if (me) {
- const user = await models.user.findUnique({ where: { id: me.id } })
- // wild west mode has everything
- if (user.wildWestMode) {
- return ''
- }
- // greeter mode includes freebies if feebies haven't been flagged
- if (user.greeterMode) {
- clause = ' AND (NOT "Item".freebie OR ("Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0 AND "Item".freebie)'
- }
-
- // always include if it's mine
- clause += ` OR "Item"."userId" = ${me.id})`
- } else {
- // close default freebie clause
- clause += ')'
- }
-
- // if the item is above the threshold or is mine
- clause += ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
- if (me) {
- clause += ` OR "Item"."userId" = ${me.id}`
- }
- clause += ')'
-
- return clause
-}
-
-function typeClause (type) {
- switch (type) {
- case 'links':
- return ' AND "Item".url IS NOT NULL AND "Item"."parentId" IS NULL'
- case 'discussions':
- return ' AND "Item".url IS NULL AND "Item".bio = false AND "Item"."pollCost" IS NULL AND "Item"."parentId" IS NULL'
- case 'polls':
- return ' AND "Item"."pollCost" IS NOT NULL AND "Item"."parentId" IS NULL'
- case 'bios':
- return ' AND "Item".bio = true AND "Item"."parentId" IS NULL'
- case 'bounties':
- return ' AND "Item".bounty IS NOT NULL AND "Item"."parentId" IS NULL'
- case 'comments':
- return ' AND "Item"."parentId" IS NOT NULL'
- case 'freebies':
- return ' AND "Item".freebie'
- case 'outlawed':
- return ` AND "Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD}`
- case 'borderland':
- return ' AND "Item"."weightedVotes" - "Item"."weightedDownVotes" < 0 '
- case 'all':
- case 'bookmarks':
- return ''
- case 'jobs':
- return ' AND "Item"."subName" = \'jobs\''
- default:
- return ' AND "Item"."parentId" IS NULL'
- }
-}
-
// this grabs all the stuff we need to display the item list and only
// hits the db once ... orderBy needs to be duplicated on the outer query because
// joining does not preserve the order of the inner query
@@ -221,13 +150,15 @@ async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args)
${orderBy}`, ...args)
} else {
return await models.$queryRawUnsafe(`
- SELECT "Item".*, to_json(users.*) as user, COALESCE("ItemAct"."meMsats", 0) as "meMsats",
+ SELECT "Item".*, to_jsonb(users.*) || jsonb_build_object('meMute', "Mute"."mutedId" IS NOT NULL) as user,
+ COALESCE("ItemAct"."meMsats", 0) as "meMsats",
COALESCE("ItemAct"."meDontLike", false) as "meDontLike", b."itemId" IS NOT NULL AS "meBookmark",
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward"
FROM (
${query}
) "Item"
JOIN users ON "Item"."userId" = users.id
+ LEFT JOIN "Mute" ON "Mute"."muterId" = ${me.id} AND "Mute"."mutedId" = "Item"."userId"
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}
@@ -243,24 +174,26 @@ async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args)
}
}
-const subClause = (sub, num, table, solo) => {
- return sub ? ` ${solo ? 'WHERE' : 'AND'} ${table ? `"${table}".` : ''}"subName" = $${num} ` : ''
-}
-
const relationClause = (type) => {
+ let clause = ''
switch (type) {
case 'comments':
- return ' FROM "Item" JOIN "Item" root ON "Item"."rootId" = root.id '
+ clause += ' FROM "Item" JOIN "Item" root ON "Item"."rootId" = root.id '
+ break
case 'bookmarks':
- return ' FROM "Item" JOIN "Bookmark" ON "Bookmark"."itemId" = "Item"."id" '
+ clause += ' FROM "Item" JOIN "Bookmark" ON "Bookmark"."itemId" = "Item"."id" '
+ break
case 'outlawed':
case 'borderland':
case 'freebies':
case 'all':
- return ' FROM "Item" LEFT JOIN "Item" root ON "Item"."rootId" = root.id '
+ clause += ' FROM "Item" LEFT JOIN "Item" root ON "Item"."rootId" = root.id '
+ break
default:
- return ' FROM "Item" '
+ clause += ' FROM "Item" '
}
+
+ return clause
}
const selectClause = (type) => type === 'bookmarks'
@@ -269,8 +202,91 @@ const selectClause = (type) => type === 'bookmarks'
const subClauseTable = (type) => COMMENT_TYPE_QUERY.includes(type) ? 'root' : 'Item'
+export const whereClause = (...clauses) => {
+ const clause = clauses.flat(Infinity).filter(c => c).join(' AND ')
+ return clause ? ` WHERE ${clause} ` : ''
+}
+
const activeOrMine = (me) => {
- return me ? ` AND ("Item".status <> 'STOPPED' OR "Item"."userId" = ${me.id}) ` : ' AND "Item".status <> \'STOPPED\' '
+ return me ? `("Item".status <> 'STOPPED' OR "Item"."userId" = ${me.id})` : '"Item".status <> \'STOPPED\''
+}
+
+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) => {
+ return sub ? `${table ? `"${table}".` : ''}"subName" = $${num}` : ''
+}
+
+export async function filterClause (me, models, type) {
+ // if you are explicitly asking for marginal content, don't filter them
+ if (['outlawed', 'borderland', 'freebies'].includes(type)) {
+ if (me && ['outlawed', 'borderland'].includes(type)) {
+ // unless the item is mine
+ return `"Item"."userId" <> ${me.id}`
+ }
+
+ return ''
+ }
+
+ // handle freebies
+ // by default don't include freebies unless they have upvotes
+ let freebieClauses = ['NOT "Item".freebie', '"Item"."weightedVotes" - "Item"."weightedDownVotes" > 0']
+ if (me) {
+ const user = await models.user.findUnique({ where: { id: me.id } })
+ // wild west mode has everything
+ if (user.wildWestMode) {
+ return ''
+ }
+ // greeter mode includes freebies if feebies haven't been flagged
+ if (user.greeterMode) {
+ freebieClauses = ['NOT "Item".freebie', '"Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0']
+ }
+
+ // always include if it's mine
+ freebieClauses.push(`"Item"."userId" = ${me.id}`)
+ }
+ const freebieClause = '(' + freebieClauses.join(' OR ') + ')'
+
+ // handle outlawed
+ // if the item is above the threshold or is mine
+ const outlawClauses = [`"Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`]
+ if (me) {
+ outlawClauses.push(`"Item"."userId" = ${me.id}`)
+ }
+ const outlawClause = '(' + outlawClauses.join(' OR ') + ')'
+
+ return [freebieClause, outlawClause]
+}
+
+function typeClause (type) {
+ switch (type) {
+ case 'links':
+ return ['"Item".url IS NOT NULL', '"Item"."parentId" IS NULL']
+ case 'discussions':
+ return ['"Item".url IS NULL', '"Item".bio = false', '"Item"."pollCost" IS NULL', '"Item"."parentId" IS NULL']
+ case 'polls':
+ return ['"Item"."pollCost" IS NOT NULL', '"Item"."parentId" IS NULL']
+ case 'bios':
+ return ['"Item".bio = true', '"Item"."parentId" IS NULL']
+ case 'bounties':
+ return ['"Item".bounty IS NOT NULL', '"Item"."parentId" IS NULL']
+ case 'comments':
+ return '"Item"."parentId" IS NOT NULL'
+ case 'freebies':
+ return '"Item".freebie'
+ case 'outlawed':
+ return `"Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD}`
+ case 'borderland':
+ return '"Item"."weightedVotes" - "Item"."weightedDownVotes" < 0'
+ case 'all':
+ case 'bookmarks':
+ return ''
+ case 'jobs':
+ return '"Item"."subName" = \'jobs\''
+ default:
+ return '"Item"."parentId" IS NULL'
+ }
}
export default {
@@ -324,12 +340,14 @@ export default {
query: `
${selectClause(type)}
${relationClause(type)}
- WHERE "${table}"."userId" = $2 AND "${table}".created_at <= $1
- ${subClause(sub, 5, subClauseTable(type))}
- ${activeOrMine(me)}
- ${await filterClause(me, models, type)}
- ${typeClause(type)}
- ${whenClause(when || 'forever', type)}
+ ${whereClause(
+ `"${table}"."userId" = $2`,
+ `"${table}".created_at <= $1`,
+ subClause(sub, 5, subClauseTable(type)),
+ activeOrMine(me),
+ await filterClause(me, models, type),
+ typeClause(type),
+ whenClause(when || 'forever', type))}
${await orderByClause(by, me, models, type)}
OFFSET $3
LIMIT $4`,
@@ -343,11 +361,14 @@ export default {
query: `
${SELECT}
${relationClause(type)}
- WHERE "Item".created_at <= $1
- ${subClause(sub, 4, subClauseTable(type))}
- ${activeOrMine(me)}
- ${await filterClause(me, models, type)}
- ${typeClause(type)}
+ ${whereClause(
+ '"Item".created_at <= $1',
+ subClause(sub, 4, subClauseTable(type)),
+ activeOrMine(me),
+ await filterClause(me, models, type),
+ typeClause(type),
+ muteClause(me)
+ )}
ORDER BY "Item".created_at DESC
OFFSET $2
LIMIT $3`,
@@ -361,12 +382,15 @@ export default {
query: `
${selectClause(type)}
${relationClause(type)}
- WHERE "Item".created_at <= $1
- AND "Item"."pinId" IS NULL AND "Item"."deletedAt" IS NULL
- ${subClause(sub, 4, subClauseTable(type))}
- ${typeClause(type)}
- ${whenClause(when, type)}
- ${await filterClause(me, models, type)}
+ ${whereClause(
+ '"Item".created_at <= $1',
+ '"Item"."pinId" IS NULL',
+ '"Item"."deletedAt" IS NULL',
+ subClause(sub, 4, subClauseTable(type)),
+ typeClause(type),
+ whenClause(when, type),
+ await filterClause(me, models, type),
+ muteClause(me))}
${await orderByClause(by || 'zaprank', me, models, type)}
OFFSET $2
LIMIT $3`,
@@ -392,10 +416,13 @@ export default {
THEN rank() OVER (ORDER BY "maxBid" DESC, created_at ASC)
ELSE rank() OVER (ORDER BY created_at DESC) END AS rank
FROM "Item"
- WHERE "parentId" IS NULL AND created_at <= $1
- AND "pinId" IS NULL
- ${subClause(sub, 4)}
- AND status IN ('ACTIVE', 'NOSATS')
+ ${whereClause(
+ '"parentId" IS NULL',
+ 'created_at <= $1',
+ '"pinId" IS NULL',
+ subClause(sub, 4),
+ "status IN ('ACTIVE', 'NOSATS')"
+ )}
ORDER BY group_rank, rank
OFFSET $2
LIMIT $3`,
@@ -410,7 +437,9 @@ export default {
${SELECT}, rank
FROM "Item"
${await joinSatRankView(me, models)}
- ${subClause(sub, 3, 'Item', true)}
+ ${whereClause(
+ subClause(sub, 3, 'Item', true),
+ muteClause(me))}
ORDER BY rank ASC
OFFSET $1
LIMIT $2`,
@@ -428,11 +457,12 @@ export default {
${SELECT},
rank() OVER (
PARTITION BY "pinId"
- ORDER BY created_at DESC
+ ORDER BY "Item".created_at DESC
)
FROM "Item"
- WHERE "pinId" IS NOT NULL
- ${subClause(sub, 1)}
+ ${whereClause(
+ '"pinId" IS NOT NULL',
+ subClause(sub, 1))}
) rank_filter WHERE RANK = 1`
}, ...subArr)
}
diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js
index 241eded2..3cd7aae1 100644
--- a/api/resolvers/notifications.js
+++ b/api/resolvers/notifications.js
@@ -1,6 +1,6 @@
import { GraphQLError } from 'graphql'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
-import { getItem, filterClause } from './item'
+import { getItem, filterClause, whereClause, muteClause } from './item'
import { getInvoice } from './wallet'
import { pushSubscriptionSchema, ssValidate } from '../../lib/validate'
import { replyToSubscription } from '../webPush'
@@ -79,8 +79,13 @@ export default {
'Reply' AS type
FROM "Item"
JOIN "Item" p ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
- WHERE p."userId" = $1 AND "Item"."userId" <> $1 AND "Item".created_at <= $2
- ${await filterClause(me, models)}
+ ${whereClause(
+ 'p."userId" = $1',
+ '"Item"."userId" <> $1',
+ '"Item".created_at <= $2',
+ await filterClause(me, models),
+ muteClause(me)
+ )}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
)
@@ -92,32 +97,36 @@ export default {
FROM "ThreadSubscription"
JOIN "Item" p ON "ThreadSubscription"."itemId" = p.id
JOIN "Item" ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
- WHERE
- "ThreadSubscription"."userId" = $1
- AND "Item"."userId" <> $1 AND "Item".created_at <= $2
- -- Only show items that have been created since subscribing to the thread
- AND "Item".created_at >= "ThreadSubscription".created_at
- -- don't notify on posts
- AND "Item"."parentId" IS NOT NULL
- ${await filterClause(me, models)}
+ ${whereClause(
+ '"ThreadSubscription"."userId" = $1',
+ '"Item"."userId" <> $1',
+ '"Item".created_at <= $2',
+ '"Item".created_at >= "ThreadSubscription".created_at',
+ '"Item"."parentId" IS NOT NULL',
+ await filterClause(me, models),
+ muteClause(me)
+ )}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
)
// User subscriptions
+ // Only include posts or comments created after the corresponding subscription was enabled, not _all_ from history
itemDrivenQueries.push(
`SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
'FollowActivity' AS type
FROM "Item"
JOIN "UserSubscription" ON "Item"."userId" = "UserSubscription"."followeeId"
- WHERE "UserSubscription"."followerId" = $1
- AND "Item".created_at <= $2
- AND (
- -- Only include posts or comments created after the corresponding subscription was enabled, not _all_ from history
+ ${whereClause(
+ '"UserSubscription"."followerId" = $1',
+ '"Item".created_at <= $2',
+ `(
("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
OR ("Item"."parentId" IS NOT NULL AND "UserSubscription"."commentsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."commentsSubscribedAt")
- )
- ${await filterClause(me, models)}
+ )`,
+ await filterClause(me, models),
+ muteClause(me)
+ )}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
)
@@ -129,10 +138,13 @@ export default {
'Mention' AS type
FROM "Mention"
JOIN "Item" ON "Mention"."itemId" = "Item".id
- WHERE "Mention"."userId" = $1
- AND "Mention".created_at <= $2
- AND "Item"."userId" <> $1
- ${await filterClause(me, models)}
+ ${whereClause(
+ '"Mention"."userId" = $1',
+ '"Mention".created_at <= $2',
+ '"Item"."userId" <> $1',
+ await filterClause(me, models),
+ muteClause(me)
+ )}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
)
diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 92155c73..2cb13ada 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -4,7 +4,7 @@ import { GraphQLError } from 'graphql'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { msatsToSats } from '../../lib/format'
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '../../lib/validate'
-import { getItem, updateItem, filterClause, createItem } from './item'
+import { getItem, updateItem, filterClause, createItem, whereClause, muteClause } from './item'
import { datePivot } from '../../lib/time'
const contributors = new Set()
@@ -286,87 +286,97 @@ export default {
// check if any votes have been cast for them since checkedNotesAt
if (user.noteItemSats) {
- const votes = await models.$queryRawUnsafe(`
- SELECT 1
- FROM "Item"
- JOIN "ItemAct" ON
- "ItemAct"."itemId" = "Item".id
- AND "ItemAct"."userId" <> "Item"."userId"
- WHERE "ItemAct".created_at > $2
- AND "Item"."userId" = $1
- AND "ItemAct".act = 'TIP'
- LIMIT 1`, me.id, lastChecked)
- if (votes.length > 0) {
+ const [newSats] = await models.$queryRawUnsafe(`
+ SELECT EXISTS(
+ SELECT *
+ FROM "Item"
+ JOIN "ItemAct" ON
+ "ItemAct"."itemId" = "Item".id
+ AND "ItemAct"."userId" <> "Item"."userId"
+ WHERE "ItemAct".created_at > $2
+ AND "Item"."userId" = $1
+ AND "ItemAct".act = 'TIP')`, me.id, lastChecked)
+ if (newSats.exists) {
return true
}
}
// check if they have any replies since checkedNotesAt
- const newReplies = await models.$queryRawUnsafe(`
- SELECT 1
+ const [newReply] = await models.$queryRawUnsafe(`
+ SELECT EXISTS(
+ SELECT *
FROM "Item"
JOIN "Item" p ON
- "Item".created_at >= p.created_at
- AND ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
- AND "Item"."userId" <> $1
- WHERE p."userId" = $1
- AND "Item".created_at > $2::timestamp(3) without time zone
- ${await filterClause(me, models)}
- LIMIT 1`, me.id, lastChecked)
- if (newReplies.length > 0) {
+ ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
+ ${whereClause(
+ 'p."userId" = $1',
+ '"Item"."userId" <> $1',
+ '"Item".created_at > $2::timestamp(3) without time zone',
+ await filterClause(me, models),
+ muteClause(me)
+ )})`, me.id, lastChecked)
+ if (newReply.exists) {
return true
}
// break out thread subscription to decrease the search space of the already expensive reply query
- const newtsubs = await models.$queryRawUnsafe(`
- SELECT 1
- FROM "ThreadSubscription"
- JOIN "Item" p ON "ThreadSubscription"."itemId" = p.id
- JOIN "Item" ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
- WHERE
- "ThreadSubscription"."userId" = $1
- AND "Item".created_at > $2::timestamp(3) without time zone
- ${await filterClause(me, models)}
- LIMIT 1`, me.id, lastChecked)
- if (newtsubs.length > 0) {
+ const [newThreadSubReply] = await models.$queryRawUnsafe(`
+ SELECT EXISTS(
+ SELECT *
+ FROM "ThreadSubscription"
+ JOIN "Item" p ON "ThreadSubscription"."itemId" = p.id
+ JOIN "Item" ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
+ ${whereClause(
+ '"ThreadSubscription"."userId" = $1',
+ '"Item".created_at > $2::timestamp(3) without time zone',
+ await filterClause(me, models),
+ muteClause(me)
+ )})`, me.id, lastChecked)
+ if (newThreadSubReply.exists) {
return true
}
- const newUserSubs = await models.$queryRawUnsafe(`
- SELECT 1
- FROM "UserSubscription"
- JOIN "Item" ON "UserSubscription"."followeeId" = "Item"."userId"
- WHERE
- "UserSubscription"."followerId" = $1
- AND "Item".created_at > $2::timestamp(3) without time zone
- AND (
- ("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
- OR ("Item"."parentId" IS NOT NULL AND "UserSubscription"."commentsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."commentsSubscribedAt")
- )
- ${await filterClause(me, models)}
- LIMIT 1`, me.id, lastChecked)
- if (newUserSubs.length > 0) {
+ const [newUserSubs] = await models.$queryRawUnsafe(`
+ SELECT EXISTS(
+ SELECT *
+ FROM "UserSubscription"
+ JOIN "Item" ON "UserSubscription"."followeeId" = "Item"."userId"
+ ${whereClause(
+ '"UserSubscription"."followerId" = $1',
+ '"Item".created_at > $2::timestamp(3) without time zone',
+ `(
+ ("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
+ OR ("Item"."parentId" IS NOT NULL AND "UserSubscription"."commentsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."commentsSubscribedAt")
+ )`,
+ await filterClause(me, models),
+ muteClause(me))})`, me.id, lastChecked)
+ if (newUserSubs.exists) {
return true
}
// check if they have any mentions since checkedNotesAt
if (user.noteMentions) {
- const newMentions = await models.$queryRawUnsafe(`
- SELECT "Item".id, "Item".created_at
+ const [newMentions] = await models.$queryRawUnsafe(`
+ SELECT EXISTS(
+ SELECT *
FROM "Mention"
JOIN "Item" ON "Mention"."itemId" = "Item".id
- WHERE "Mention"."userId" = $1
- AND "Mention".created_at > $2
- AND "Item"."userId" <> $1
- LIMIT 1`, me.id, lastChecked)
- if (newMentions.length > 0) {
+ ${whereClause(
+ '"Mention"."userId" = $1',
+ '"Mention".created_at > $2',
+ '"Item"."userId" <> $1',
+ await filterClause(me, models),
+ muteClause(me)
+ )})`, me.id, lastChecked)
+ if (newMentions.exists) {
return true
}
}
if (user.noteForwardedSats) {
- const votes = await models.$queryRawUnsafe(`
- SELECT 1
+ const [newFwdSats] = await models.$queryRawUnsafe(`
+ SELECT EXISTS(
+ SELECT *
FROM "Item"
JOIN "ItemAct" ON
"ItemAct"."itemId" = "Item".id
@@ -376,9 +386,8 @@ export default {
AND "ItemForward"."userId" = $1
WHERE "ItemAct".created_at > $2
AND "Item"."userId" <> $1
- AND "ItemAct".act = 'TIP'
- LIMIT 1`, me.id, lastChecked)
- if (votes.length > 0) {
+ AND "ItemAct".act = 'TIP')`, me.id, lastChecked)
+ if (newFwdSats.exists) {
return true
}
}
@@ -432,13 +441,13 @@ export default {
// check if new invites have been redeemed
if (user.noteInvites) {
- const newInvitees = await models.$queryRawUnsafe(`
- SELECT "Invite".id
- FROM users JOIN "Invite" on users."inviteId" = "Invite".id
- WHERE "Invite"."userId" = $1
- AND users.created_at > $2
- LIMIT 1`, me.id, lastChecked)
- if (newInvitees.length > 0) {
+ const [newInvites] = await models.$queryRawUnsafe(`
+ SELECT EXISTS(
+ SELECT *
+ FROM users JOIN "Invite" on users."inviteId" = "Invite".id
+ WHERE "Invite"."userId" = $1
+ AND users.created_at > $2)`, me.id, lastChecked)
+ if (newInvites.exists) {
return true
}
@@ -626,6 +635,17 @@ export default {
}
return { id }
},
+ toggleMute: async (parent, { id }, { me, models }) => {
+ const lookupData = { muterId: Number(me.id), mutedId: Number(id) }
+ const where = { muterId_mutedId: lookupData }
+ const existing = await models.mute.findUnique({ where })
+ if (existing) {
+ await models.mute.delete({ where })
+ } else {
+ await models.mute.create({ data: { ...lookupData } })
+ }
+ return { id }
+ },
hideWelcomeBanner: async (parent, data, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
@@ -670,6 +690,21 @@ export default {
}
})
},
+ meMute: async (user, args, { me, models }) => {
+ if (!me) return false
+ if (typeof user.meMute !== 'undefined') return user.meMute
+
+ const mute = await models.mute.findUnique({
+ where: {
+ muterId_mutedId: {
+ muterId: Number(me.id),
+ mutedId: Number(user.id)
+ }
+ }
+ })
+
+ return !!mute
+ },
nposts: async (user, { when }, { models }) => {
if (typeof user.nposts !== 'undefined') {
return user.nposts
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 82674c36..26574d87 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -34,6 +34,7 @@ export default gql`
hideWelcomeBanner: Boolean
subscribeUserPosts(id: ID): User
subscribeUserComments(id: ID): User
+ toggleMute(id: ID): User
}
type AuthMethods {
@@ -97,5 +98,6 @@ export default gql`
hideIsContributor: Boolean!
meSubscriptionPosts: Boolean!
meSubscriptionComments: Boolean!
+ meMute: Boolean
}
`
diff --git a/components/comment.js b/components/comment.js
index 607e4700..d92eab6c 100644
--- a/components/comment.js
+++ b/components/comment.js
@@ -99,9 +99,9 @@ export default function Comment ({
}) {
const [edit, setEdit] = useState()
const me = useMe()
+ const isHiddenFreebie = !me?.wildWestMode && !me?.greeterMode && !item.mine && item.freebie && item.wvotes <= 0
const [collapse, setCollapse] = useState(
- !me?.wildWestMode && !me?.greeterMode &&
- !item.mine && item.freebie && item.wvotes <= 0
+ isHiddenFreebie || item?.user?.meMute
? 'yep'
: 'nope')
const ref = useRef(null)
@@ -149,25 +149,35 @@ export default function Comment ({
: