mutes
This commit is contained in:
parent
b6cb895871
commit
4bd489a36a
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
)
|
||||
|
|
|
@ -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
|
||||
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'
|
||||
LIMIT 1`, me.id, lastChecked)
|
||||
if (votes.length > 0) {
|
||||
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
|
||||
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'}
|
||||
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) {
|
||||
${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
|
||||
const [newUserSubs] = await models.$queryRawUnsafe(`
|
||||
SELECT EXISTS(
|
||||
SELECT *
|
||||
FROM "UserSubscription"
|
||||
JOIN "Item" ON "UserSubscription"."followeeId" = "Item"."userId"
|
||||
WHERE
|
||||
"UserSubscription"."followerId" = $1
|
||||
AND "Item".created_at > $2::timestamp(3) without time zone
|
||||
AND (
|
||||
${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)}
|
||||
LIMIT 1`, me.id, lastChecked)
|
||||
if (newUserSubs.length > 0) {
|
||||
)`,
|
||||
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
|
||||
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
|
||||
LIMIT 1`, me.id, lastChecked)
|
||||
if (newInvitees.length > 0) {
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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,7 +149,16 @@ export default function Comment ({
|
|||
: <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
|
||||
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
||||
<div className='d-flex align-items-center'>
|
||||
<ItemInfo
|
||||
{item.user?.meMute && !includeParent && collapse === 'yep'
|
||||
? (
|
||||
<span
|
||||
className={`${itemStyles.other} ${styles.other} pointer`} onClick={() => {
|
||||
setCollapse('nope')
|
||||
window.localStorage.setItem(`commentCollapse:${item.id}`, 'nope')
|
||||
}}
|
||||
>reply from someone you muted
|
||||
</span>)
|
||||
: <ItemInfo
|
||||
item={item}
|
||||
pendingSats={pendingSats}
|
||||
commentsText='replies'
|
||||
|
@ -167,7 +176,8 @@ export default function Comment ({
|
|||
}
|
||||
onEdit={e => { setEdit(!edit) }}
|
||||
editText={edit ? 'cancel' : 'edit'}
|
||||
/>
|
||||
/>}
|
||||
|
||||
{!includeParent && (collapse === 'yep'
|
||||
? <Eye
|
||||
className={styles.collapser} height={10} width={10} onClick={() => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { CopyLinkDropdownItem } from './share'
|
|||
import Hat from './hat'
|
||||
import { AD_USER_ID } from '../lib/constants'
|
||||
import ActionDropdown from './action-dropdown'
|
||||
import MuteDropdownItem from './mute'
|
||||
|
||||
export default function ItemInfo ({
|
||||
item, pendingSats, full, commentsText = 'comments',
|
||||
|
@ -131,15 +132,20 @@ export default function ItemInfo ({
|
|||
<ActionDropdown>
|
||||
<CopyLinkDropdownItem item={item} />
|
||||
{me && <BookmarkDropdownItem item={item} />}
|
||||
{me && item.user.id !== me.id && <SubscribeDropdownItem item={item} />}
|
||||
{me && !item.mine && <SubscribeDropdownItem item={item} />}
|
||||
{item.otsHash &&
|
||||
<Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
|
||||
ots timestamp
|
||||
opentimestamp
|
||||
</Link>}
|
||||
{me && !item.meSats && !item.position &&
|
||||
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />}
|
||||
{item.mine && !item.position && !item.deletedAt &&
|
||||
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />}
|
||||
{me && !item.mine &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<MuteDropdownItem user={item.user} />
|
||||
</>}
|
||||
</ActionDropdown>
|
||||
{extraInfo}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { useMutation } from '@apollo/client'
|
||||
import { gql } from 'graphql-tag'
|
||||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
import { useToast } from './toast'
|
||||
|
||||
export default function MuteDropdownItem ({ user: { name, id, meMute } }) {
|
||||
const toaster = useToast()
|
||||
const [toggleMute] = useMutation(
|
||||
gql`
|
||||
mutation toggleMute($id: ID!) {
|
||||
toggleMute(id: $id) {
|
||||
meMute
|
||||
}
|
||||
}`, {
|
||||
update (cache, { data: { toggleMute } }) {
|
||||
cache.modify({
|
||||
id: `User:${id}`,
|
||||
fields: {
|
||||
meMute: () => toggleMute.meMute
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
return (
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
try {
|
||||
await toggleMute({ variables: { id } })
|
||||
toaster.success(`${meMute ? 'un' : ''}muted ${name}`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger(`failed to ${meMute ? 'un' : ''}mute ${name}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{`${meMute ? 'un' : ''}mute ${name}`}
|
||||
</Dropdown.Item>
|
||||
)
|
||||
}
|
|
@ -21,6 +21,7 @@ import Hat from './hat'
|
|||
import SubscribeUserDropdownItem from './subscribeUser'
|
||||
import ActionDropdown from './action-dropdown'
|
||||
import CodeIcon from '../svgs/terminal-box-fill.svg'
|
||||
import MuteDropdownItem from './mute'
|
||||
|
||||
export default function UserHeader ({ user }) {
|
||||
const router = useRouter()
|
||||
|
@ -156,11 +157,12 @@ function NymView ({ user, isMe, setEditting }) {
|
|||
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
||||
{isMe &&
|
||||
<Button className='py-0' style={{ lineHeight: '1.25' }} variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
|
||||
{!isMe &&
|
||||
{!isMe && me &&
|
||||
<div className='ms-2'>
|
||||
<ActionDropdown>
|
||||
{me && <SubscribeUserDropdownItem user={user} target='posts' />}
|
||||
{me && <SubscribeUserDropdownItem user={user} target='comments' />}
|
||||
<SubscribeUserDropdownItem user={user} target='posts' />
|
||||
<SubscribeUserDropdownItem user={user} target='comments' />
|
||||
<MuteDropdownItem user={user} />
|
||||
</ActionDropdown>
|
||||
</div>}
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,7 @@ export const COMMENT_FIELDS = gql`
|
|||
streak
|
||||
hideCowboyHat
|
||||
id
|
||||
meMute
|
||||
}
|
||||
sats
|
||||
meAnonSats @client
|
||||
|
|
|
@ -14,6 +14,7 @@ export const ITEM_FIELDS = gql`
|
|||
streak
|
||||
hideCowboyHat
|
||||
id
|
||||
meMute
|
||||
}
|
||||
otsHash
|
||||
position
|
||||
|
|
|
@ -158,6 +158,7 @@ export const USER_FIELDS = gql`
|
|||
isContributor
|
||||
meSubscriptionPosts
|
||||
meSubscriptionComments
|
||||
meMute
|
||||
}`
|
||||
|
||||
export const TOP_USERS = gql`
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Mute" (
|
||||
"muterId" INTEGER NOT NULL,
|
||||
"mutedId" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Mute_pkey" PRIMARY KEY ("muterId","mutedId")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Mute_mutedId_muterId_idx" ON "Mute"("mutedId", "muterId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Mute" ADD CONSTRAINT "Mute_muterId_fkey" FOREIGN KEY ("muterId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Mute" ADD CONSTRAINT "Mute_mutedId_fkey" FOREIGN KEY ("mutedId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -0,0 +1,40 @@
|
|||
CREATE OR REPLACE FUNCTION item_comments_with_me(_item_id int, _me_id int, _level int, _where text, _order_by text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql STABLE PARALLEL SAFE AS
|
||||
$$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
BEGIN
|
||||
IF _level < 1 THEN
|
||||
RETURN '[]'::jsonb;
|
||||
END IF;
|
||||
|
||||
EXECUTE ''
|
||||
|| 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
|
||||
|| 'FROM ( '
|
||||
|| ' SELECT "Item".*, "Item".created_at at time zone ''UTC'' AS "createdAt", "Item".updated_at at time zone ''UTC'' AS "updatedAt", '
|
||||
|| ' item_comments_with_me("Item".id, $5, $2 - 1, $3, $4) AS comments, '
|
||||
|| ' 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", '
|
||||
|| ' "Bookmark"."itemId" IS NOT NULL AS "meBookmark", "ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription" '
|
||||
|| ' FROM "Item" p '
|
||||
|| ' JOIN "Item" ON "Item"."parentId" = p.id '
|
||||
|| ' JOIN users ON users.id = "Item"."userId" '
|
||||
|| ' LEFT JOIN "Mute" ON "Mute"."muterId" = $5 AND "Mute"."mutedId" = "Item"."userId"'
|
||||
|| ' LEFT JOIN "Bookmark" ON "Bookmark"."itemId" = "Item".id AND "Bookmark"."userId" = $5 '
|
||||
|| ' LEFT JOIN "ThreadSubscription" ON "ThreadSubscription"."itemId" = "Item".id AND "ThreadSubscription"."userId" = $5 '
|
||||
|| ' LEFT JOIN LATERAL ( '
|
||||
|| ' SELECT "itemId", sum("ItemAct".msats) FILTER (WHERE act = ''FEE'' OR act = ''TIP'') AS "meMsats", '
|
||||
|| ' bool_or(act = ''DONT_LIKE_THIS'') AS "meDontLike" '
|
||||
|| ' FROM "ItemAct" '
|
||||
|| ' WHERE "ItemAct"."userId" = $5 '
|
||||
|| ' AND "ItemAct"."itemId" = "Item".id '
|
||||
|| ' GROUP BY "ItemAct"."itemId" '
|
||||
|| ' ) "ItemAct" ON true '
|
||||
|| ' WHERE p.id = $1 ' || _where || ' '
|
||||
|| _order_by
|
||||
|| ' ) sub'
|
||||
INTO result USING _item_id, _level, _where, _order_by, _me_id;
|
||||
RETURN result;
|
||||
END
|
||||
$$;
|
|
@ -92,12 +92,26 @@ model User {
|
|||
hideWelcomeBanner Boolean @default(false)
|
||||
diagnostics Boolean @default(false)
|
||||
hideIsContributor Boolean @default(false)
|
||||
muters Mute[] @relation("muter")
|
||||
muteds Mute[] @relation("muted")
|
||||
|
||||
@@index([createdAt], map: "users.created_at_index")
|
||||
@@index([inviteId], map: "users.inviteId_index")
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Mute {
|
||||
muterId Int
|
||||
mutedId Int
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
muter User @relation("muter", fields: [muterId], references: [id], onDelete: Cascade)
|
||||
muted User @relation("muted", fields: [mutedId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([muterId, mutedId])
|
||||
@@index([mutedId, muterId])
|
||||
}
|
||||
|
||||
model Streak {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
@ -539,7 +553,7 @@ model UserSubscription {
|
|||
followerId Int
|
||||
followeeId Int
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @updatedAt
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
postsSubscribedAt DateTime?
|
||||
commentsSubscribedAt DateTime?
|
||||
follower User @relation("follower", fields: [followerId], references: [id], onDelete: Cascade)
|
||||
|
|
Loading…
Reference in New Issue