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'
|
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
|
// 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
|
// hits the db once ... orderBy needs to be duplicated on the outer query because
|
||||||
// joining does not preserve the order of the inner query
|
// joining does not preserve the order of the inner query
|
||||||
|
@ -221,13 +150,15 @@ async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args)
|
||||||
${orderBy}`, ...args)
|
${orderBy}`, ...args)
|
||||||
} else {
|
} else {
|
||||||
return await models.$queryRawUnsafe(`
|
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",
|
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"
|
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward"
|
||||||
FROM (
|
FROM (
|
||||||
${query}
|
${query}
|
||||||
) "Item"
|
) "Item"
|
||||||
JOIN users ON "Item"."userId" = users.id
|
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 "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 "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 "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) => {
|
const relationClause = (type) => {
|
||||||
|
let clause = ''
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'comments':
|
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':
|
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 'outlawed':
|
||||||
case 'borderland':
|
case 'borderland':
|
||||||
case 'freebies':
|
case 'freebies':
|
||||||
case 'all':
|
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:
|
default:
|
||||||
return ' FROM "Item" '
|
clause += ' FROM "Item" '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return clause
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectClause = (type) => type === 'bookmarks'
|
const selectClause = (type) => type === 'bookmarks'
|
||||||
|
@ -269,8 +202,91 @@ const selectClause = (type) => type === 'bookmarks'
|
||||||
|
|
||||||
const subClauseTable = (type) => COMMENT_TYPE_QUERY.includes(type) ? 'root' : 'Item'
|
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) => {
|
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 {
|
export default {
|
||||||
|
@ -324,12 +340,14 @@ export default {
|
||||||
query: `
|
query: `
|
||||||
${selectClause(type)}
|
${selectClause(type)}
|
||||||
${relationClause(type)}
|
${relationClause(type)}
|
||||||
WHERE "${table}"."userId" = $2 AND "${table}".created_at <= $1
|
${whereClause(
|
||||||
${subClause(sub, 5, subClauseTable(type))}
|
`"${table}"."userId" = $2`,
|
||||||
${activeOrMine(me)}
|
`"${table}".created_at <= $1`,
|
||||||
${await filterClause(me, models, type)}
|
subClause(sub, 5, subClauseTable(type)),
|
||||||
${typeClause(type)}
|
activeOrMine(me),
|
||||||
${whenClause(when || 'forever', type)}
|
await filterClause(me, models, type),
|
||||||
|
typeClause(type),
|
||||||
|
whenClause(when || 'forever', type))}
|
||||||
${await orderByClause(by, me, models, type)}
|
${await orderByClause(by, me, models, type)}
|
||||||
OFFSET $3
|
OFFSET $3
|
||||||
LIMIT $4`,
|
LIMIT $4`,
|
||||||
|
@ -343,11 +361,14 @@ export default {
|
||||||
query: `
|
query: `
|
||||||
${SELECT}
|
${SELECT}
|
||||||
${relationClause(type)}
|
${relationClause(type)}
|
||||||
WHERE "Item".created_at <= $1
|
${whereClause(
|
||||||
${subClause(sub, 4, subClauseTable(type))}
|
'"Item".created_at <= $1',
|
||||||
${activeOrMine(me)}
|
subClause(sub, 4, subClauseTable(type)),
|
||||||
${await filterClause(me, models, type)}
|
activeOrMine(me),
|
||||||
${typeClause(type)}
|
await filterClause(me, models, type),
|
||||||
|
typeClause(type),
|
||||||
|
muteClause(me)
|
||||||
|
)}
|
||||||
ORDER BY "Item".created_at DESC
|
ORDER BY "Item".created_at DESC
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
LIMIT $3`,
|
LIMIT $3`,
|
||||||
|
@ -361,12 +382,15 @@ export default {
|
||||||
query: `
|
query: `
|
||||||
${selectClause(type)}
|
${selectClause(type)}
|
||||||
${relationClause(type)}
|
${relationClause(type)}
|
||||||
WHERE "Item".created_at <= $1
|
${whereClause(
|
||||||
AND "Item"."pinId" IS NULL AND "Item"."deletedAt" IS NULL
|
'"Item".created_at <= $1',
|
||||||
${subClause(sub, 4, subClauseTable(type))}
|
'"Item"."pinId" IS NULL',
|
||||||
${typeClause(type)}
|
'"Item"."deletedAt" IS NULL',
|
||||||
${whenClause(when, type)}
|
subClause(sub, 4, subClauseTable(type)),
|
||||||
${await filterClause(me, models, type)}
|
typeClause(type),
|
||||||
|
whenClause(when, type),
|
||||||
|
await filterClause(me, models, type),
|
||||||
|
muteClause(me))}
|
||||||
${await orderByClause(by || 'zaprank', me, models, type)}
|
${await orderByClause(by || 'zaprank', me, models, type)}
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
LIMIT $3`,
|
LIMIT $3`,
|
||||||
|
@ -392,10 +416,13 @@ export default {
|
||||||
THEN rank() OVER (ORDER BY "maxBid" DESC, created_at ASC)
|
THEN rank() OVER (ORDER BY "maxBid" DESC, created_at ASC)
|
||||||
ELSE rank() OVER (ORDER BY created_at DESC) END AS rank
|
ELSE rank() OVER (ORDER BY created_at DESC) END AS rank
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
WHERE "parentId" IS NULL AND created_at <= $1
|
${whereClause(
|
||||||
AND "pinId" IS NULL
|
'"parentId" IS NULL',
|
||||||
${subClause(sub, 4)}
|
'created_at <= $1',
|
||||||
AND status IN ('ACTIVE', 'NOSATS')
|
'"pinId" IS NULL',
|
||||||
|
subClause(sub, 4),
|
||||||
|
"status IN ('ACTIVE', 'NOSATS')"
|
||||||
|
)}
|
||||||
ORDER BY group_rank, rank
|
ORDER BY group_rank, rank
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
LIMIT $3`,
|
LIMIT $3`,
|
||||||
|
@ -410,7 +437,9 @@ export default {
|
||||||
${SELECT}, rank
|
${SELECT}, rank
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
${await joinSatRankView(me, models)}
|
${await joinSatRankView(me, models)}
|
||||||
${subClause(sub, 3, 'Item', true)}
|
${whereClause(
|
||||||
|
subClause(sub, 3, 'Item', true),
|
||||||
|
muteClause(me))}
|
||||||
ORDER BY rank ASC
|
ORDER BY rank ASC
|
||||||
OFFSET $1
|
OFFSET $1
|
||||||
LIMIT $2`,
|
LIMIT $2`,
|
||||||
|
@ -428,11 +457,12 @@ export default {
|
||||||
${SELECT},
|
${SELECT},
|
||||||
rank() OVER (
|
rank() OVER (
|
||||||
PARTITION BY "pinId"
|
PARTITION BY "pinId"
|
||||||
ORDER BY created_at DESC
|
ORDER BY "Item".created_at DESC
|
||||||
)
|
)
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
WHERE "pinId" IS NOT NULL
|
${whereClause(
|
||||||
${subClause(sub, 1)}
|
'"pinId" IS NOT NULL',
|
||||||
|
subClause(sub, 1))}
|
||||||
) rank_filter WHERE RANK = 1`
|
) rank_filter WHERE RANK = 1`
|
||||||
}, ...subArr)
|
}, ...subArr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||||
import { getItem, filterClause } from './item'
|
import { getItem, filterClause, whereClause, muteClause } from './item'
|
||||||
import { getInvoice } from './wallet'
|
import { getInvoice } from './wallet'
|
||||||
import { pushSubscriptionSchema, ssValidate } from '../../lib/validate'
|
import { pushSubscriptionSchema, ssValidate } from '../../lib/validate'
|
||||||
import { replyToSubscription } from '../webPush'
|
import { replyToSubscription } from '../webPush'
|
||||||
|
@ -79,8 +79,13 @@ export default {
|
||||||
'Reply' AS type
|
'Reply' AS type
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
JOIN "Item" p ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
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
|
${whereClause(
|
||||||
${await filterClause(me, models)}
|
'p."userId" = $1',
|
||||||
|
'"Item"."userId" <> $1',
|
||||||
|
'"Item".created_at <= $2',
|
||||||
|
await filterClause(me, models),
|
||||||
|
muteClause(me)
|
||||||
|
)}
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT}+$3`
|
LIMIT ${LIMIT}+$3`
|
||||||
)
|
)
|
||||||
|
@ -92,32 +97,36 @@ export default {
|
||||||
FROM "ThreadSubscription"
|
FROM "ThreadSubscription"
|
||||||
JOIN "Item" p ON "ThreadSubscription"."itemId" = p.id
|
JOIN "Item" p ON "ThreadSubscription"."itemId" = p.id
|
||||||
JOIN "Item" ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
JOIN "Item" ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
||||||
WHERE
|
${whereClause(
|
||||||
"ThreadSubscription"."userId" = $1
|
'"ThreadSubscription"."userId" = $1',
|
||||||
AND "Item"."userId" <> $1 AND "Item".created_at <= $2
|
'"Item"."userId" <> $1',
|
||||||
-- Only show items that have been created since subscribing to the thread
|
'"Item".created_at <= $2',
|
||||||
AND "Item".created_at >= "ThreadSubscription".created_at
|
'"Item".created_at >= "ThreadSubscription".created_at',
|
||||||
-- don't notify on posts
|
'"Item"."parentId" IS NOT NULL',
|
||||||
AND "Item"."parentId" IS NOT NULL
|
await filterClause(me, models),
|
||||||
${await filterClause(me, models)}
|
muteClause(me)
|
||||||
|
)}
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT}+$3`
|
LIMIT ${LIMIT}+$3`
|
||||||
)
|
)
|
||||||
|
|
||||||
// User subscriptions
|
// User subscriptions
|
||||||
|
// Only include posts or comments created after the corresponding subscription was enabled, not _all_ from history
|
||||||
itemDrivenQueries.push(
|
itemDrivenQueries.push(
|
||||||
`SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
|
`SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
|
||||||
'FollowActivity' AS type
|
'FollowActivity' AS type
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
JOIN "UserSubscription" ON "Item"."userId" = "UserSubscription"."followeeId"
|
JOIN "UserSubscription" ON "Item"."userId" = "UserSubscription"."followeeId"
|
||||||
WHERE "UserSubscription"."followerId" = $1
|
${whereClause(
|
||||||
AND "Item".created_at <= $2
|
'"UserSubscription"."followerId" = $1',
|
||||||
AND (
|
'"Item".created_at <= $2',
|
||||||
-- Only include posts or comments created after the corresponding subscription was enabled, not _all_ from history
|
`(
|
||||||
("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
|
("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")
|
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
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT}+$3`
|
LIMIT ${LIMIT}+$3`
|
||||||
)
|
)
|
||||||
|
@ -129,10 +138,13 @@ export default {
|
||||||
'Mention' AS type
|
'Mention' AS type
|
||||||
FROM "Mention"
|
FROM "Mention"
|
||||||
JOIN "Item" ON "Mention"."itemId" = "Item".id
|
JOIN "Item" ON "Mention"."itemId" = "Item".id
|
||||||
WHERE "Mention"."userId" = $1
|
${whereClause(
|
||||||
AND "Mention".created_at <= $2
|
'"Mention"."userId" = $1',
|
||||||
AND "Item"."userId" <> $1
|
'"Mention".created_at <= $2',
|
||||||
${await filterClause(me, models)}
|
'"Item"."userId" <> $1',
|
||||||
|
await filterClause(me, models),
|
||||||
|
muteClause(me)
|
||||||
|
)}
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT}+$3`
|
LIMIT ${LIMIT}+$3`
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { GraphQLError } from 'graphql'
|
||||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||||
import { msatsToSats } from '../../lib/format'
|
import { msatsToSats } from '../../lib/format'
|
||||||
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '../../lib/validate'
|
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'
|
import { datePivot } from '../../lib/time'
|
||||||
|
|
||||||
const contributors = new Set()
|
const contributors = new Set()
|
||||||
|
@ -286,87 +286,97 @@ export default {
|
||||||
|
|
||||||
// check if any votes have been cast for them since checkedNotesAt
|
// check if any votes have been cast for them since checkedNotesAt
|
||||||
if (user.noteItemSats) {
|
if (user.noteItemSats) {
|
||||||
const votes = await models.$queryRawUnsafe(`
|
const [newSats] = await models.$queryRawUnsafe(`
|
||||||
SELECT 1
|
SELECT EXISTS(
|
||||||
FROM "Item"
|
SELECT *
|
||||||
JOIN "ItemAct" ON
|
FROM "Item"
|
||||||
"ItemAct"."itemId" = "Item".id
|
JOIN "ItemAct" ON
|
||||||
AND "ItemAct"."userId" <> "Item"."userId"
|
"ItemAct"."itemId" = "Item".id
|
||||||
WHERE "ItemAct".created_at > $2
|
AND "ItemAct"."userId" <> "Item"."userId"
|
||||||
AND "Item"."userId" = $1
|
WHERE "ItemAct".created_at > $2
|
||||||
AND "ItemAct".act = 'TIP'
|
AND "Item"."userId" = $1
|
||||||
LIMIT 1`, me.id, lastChecked)
|
AND "ItemAct".act = 'TIP')`, me.id, lastChecked)
|
||||||
if (votes.length > 0) {
|
if (newSats.exists) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if they have any replies since checkedNotesAt
|
// check if they have any replies since checkedNotesAt
|
||||||
const newReplies = await models.$queryRawUnsafe(`
|
const [newReply] = await models.$queryRawUnsafe(`
|
||||||
SELECT 1
|
SELECT EXISTS(
|
||||||
|
SELECT *
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
JOIN "Item" p ON
|
JOIN "Item" p ON
|
||||||
"Item".created_at >= p.created_at
|
${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
||||||
AND ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
${whereClause(
|
||||||
AND "Item"."userId" <> $1
|
'p."userId" = $1',
|
||||||
WHERE p."userId" = $1
|
'"Item"."userId" <> $1',
|
||||||
AND "Item".created_at > $2::timestamp(3) without time zone
|
'"Item".created_at > $2::timestamp(3) without time zone',
|
||||||
${await filterClause(me, models)}
|
await filterClause(me, models),
|
||||||
LIMIT 1`, me.id, lastChecked)
|
muteClause(me)
|
||||||
if (newReplies.length > 0) {
|
)})`, me.id, lastChecked)
|
||||||
|
if (newReply.exists) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// break out thread subscription to decrease the search space of the already expensive reply query
|
// break out thread subscription to decrease the search space of the already expensive reply query
|
||||||
const newtsubs = await models.$queryRawUnsafe(`
|
const [newThreadSubReply] = await models.$queryRawUnsafe(`
|
||||||
SELECT 1
|
SELECT EXISTS(
|
||||||
FROM "ThreadSubscription"
|
SELECT *
|
||||||
JOIN "Item" p ON "ThreadSubscription"."itemId" = p.id
|
FROM "ThreadSubscription"
|
||||||
JOIN "Item" ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
JOIN "Item" p ON "ThreadSubscription"."itemId" = p.id
|
||||||
WHERE
|
JOIN "Item" ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
||||||
"ThreadSubscription"."userId" = $1
|
${whereClause(
|
||||||
AND "Item".created_at > $2::timestamp(3) without time zone
|
'"ThreadSubscription"."userId" = $1',
|
||||||
${await filterClause(me, models)}
|
'"Item".created_at > $2::timestamp(3) without time zone',
|
||||||
LIMIT 1`, me.id, lastChecked)
|
await filterClause(me, models),
|
||||||
if (newtsubs.length > 0) {
|
muteClause(me)
|
||||||
|
)})`, me.id, lastChecked)
|
||||||
|
if (newThreadSubReply.exists) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUserSubs = await models.$queryRawUnsafe(`
|
const [newUserSubs] = await models.$queryRawUnsafe(`
|
||||||
SELECT 1
|
SELECT EXISTS(
|
||||||
FROM "UserSubscription"
|
SELECT *
|
||||||
JOIN "Item" ON "UserSubscription"."followeeId" = "Item"."userId"
|
FROM "UserSubscription"
|
||||||
WHERE
|
JOIN "Item" ON "UserSubscription"."followeeId" = "Item"."userId"
|
||||||
"UserSubscription"."followerId" = $1
|
${whereClause(
|
||||||
AND "Item".created_at > $2::timestamp(3) without time zone
|
'"UserSubscription"."followerId" = $1',
|
||||||
AND (
|
'"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")
|
("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)
|
await filterClause(me, models),
|
||||||
if (newUserSubs.length > 0) {
|
muteClause(me))})`, me.id, lastChecked)
|
||||||
|
if (newUserSubs.exists) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if they have any mentions since checkedNotesAt
|
// check if they have any mentions since checkedNotesAt
|
||||||
if (user.noteMentions) {
|
if (user.noteMentions) {
|
||||||
const newMentions = await models.$queryRawUnsafe(`
|
const [newMentions] = await models.$queryRawUnsafe(`
|
||||||
SELECT "Item".id, "Item".created_at
|
SELECT EXISTS(
|
||||||
|
SELECT *
|
||||||
FROM "Mention"
|
FROM "Mention"
|
||||||
JOIN "Item" ON "Mention"."itemId" = "Item".id
|
JOIN "Item" ON "Mention"."itemId" = "Item".id
|
||||||
WHERE "Mention"."userId" = $1
|
${whereClause(
|
||||||
AND "Mention".created_at > $2
|
'"Mention"."userId" = $1',
|
||||||
AND "Item"."userId" <> $1
|
'"Mention".created_at > $2',
|
||||||
LIMIT 1`, me.id, lastChecked)
|
'"Item"."userId" <> $1',
|
||||||
if (newMentions.length > 0) {
|
await filterClause(me, models),
|
||||||
|
muteClause(me)
|
||||||
|
)})`, me.id, lastChecked)
|
||||||
|
if (newMentions.exists) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.noteForwardedSats) {
|
if (user.noteForwardedSats) {
|
||||||
const votes = await models.$queryRawUnsafe(`
|
const [newFwdSats] = await models.$queryRawUnsafe(`
|
||||||
SELECT 1
|
SELECT EXISTS(
|
||||||
|
SELECT *
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
JOIN "ItemAct" ON
|
JOIN "ItemAct" ON
|
||||||
"ItemAct"."itemId" = "Item".id
|
"ItemAct"."itemId" = "Item".id
|
||||||
|
@ -376,9 +386,8 @@ export default {
|
||||||
AND "ItemForward"."userId" = $1
|
AND "ItemForward"."userId" = $1
|
||||||
WHERE "ItemAct".created_at > $2
|
WHERE "ItemAct".created_at > $2
|
||||||
AND "Item"."userId" <> $1
|
AND "Item"."userId" <> $1
|
||||||
AND "ItemAct".act = 'TIP'
|
AND "ItemAct".act = 'TIP')`, me.id, lastChecked)
|
||||||
LIMIT 1`, me.id, lastChecked)
|
if (newFwdSats.exists) {
|
||||||
if (votes.length > 0) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -432,13 +441,13 @@ export default {
|
||||||
|
|
||||||
// check if new invites have been redeemed
|
// check if new invites have been redeemed
|
||||||
if (user.noteInvites) {
|
if (user.noteInvites) {
|
||||||
const newInvitees = await models.$queryRawUnsafe(`
|
const [newInvites] = await models.$queryRawUnsafe(`
|
||||||
SELECT "Invite".id
|
SELECT EXISTS(
|
||||||
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
|
SELECT *
|
||||||
WHERE "Invite"."userId" = $1
|
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
|
||||||
AND users.created_at > $2
|
WHERE "Invite"."userId" = $1
|
||||||
LIMIT 1`, me.id, lastChecked)
|
AND users.created_at > $2)`, me.id, lastChecked)
|
||||||
if (newInvitees.length > 0) {
|
if (newInvites.exists) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -626,6 +635,17 @@ export default {
|
||||||
}
|
}
|
||||||
return { id }
|
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 }) => {
|
hideWelcomeBanner: async (parent, data, { me, models }) => {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
|
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 }) => {
|
nposts: async (user, { when }, { models }) => {
|
||||||
if (typeof user.nposts !== 'undefined') {
|
if (typeof user.nposts !== 'undefined') {
|
||||||
return user.nposts
|
return user.nposts
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default gql`
|
||||||
hideWelcomeBanner: Boolean
|
hideWelcomeBanner: Boolean
|
||||||
subscribeUserPosts(id: ID): User
|
subscribeUserPosts(id: ID): User
|
||||||
subscribeUserComments(id: ID): User
|
subscribeUserComments(id: ID): User
|
||||||
|
toggleMute(id: ID): User
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthMethods {
|
type AuthMethods {
|
||||||
|
@ -97,5 +98,6 @@ export default gql`
|
||||||
hideIsContributor: Boolean!
|
hideIsContributor: Boolean!
|
||||||
meSubscriptionPosts: Boolean!
|
meSubscriptionPosts: Boolean!
|
||||||
meSubscriptionComments: Boolean!
|
meSubscriptionComments: Boolean!
|
||||||
|
meMute: Boolean
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -99,9 +99,9 @@ export default function Comment ({
|
||||||
}) {
|
}) {
|
||||||
const [edit, setEdit] = useState()
|
const [edit, setEdit] = useState()
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
const isHiddenFreebie = !me?.wildWestMode && !me?.greeterMode && !item.mine && item.freebie && item.wvotes <= 0
|
||||||
const [collapse, setCollapse] = useState(
|
const [collapse, setCollapse] = useState(
|
||||||
!me?.wildWestMode && !me?.greeterMode &&
|
isHiddenFreebie || item?.user?.meMute
|
||||||
!item.mine && item.freebie && item.wvotes <= 0
|
|
||||||
? 'yep'
|
? 'yep'
|
||||||
: 'nope')
|
: 'nope')
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
|
@ -149,25 +149,35 @@ export default function Comment ({
|
||||||
: <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
|
: <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
|
||||||
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
<ItemInfo
|
{item.user?.meMute && !includeParent && collapse === 'yep'
|
||||||
item={item}
|
? (
|
||||||
pendingSats={pendingSats}
|
<span
|
||||||
commentsText='replies'
|
className={`${itemStyles.other} ${styles.other} pointer`} onClick={() => {
|
||||||
commentTextSingular='reply'
|
setCollapse('nope')
|
||||||
className={`${itemStyles.other} ${styles.other}`}
|
window.localStorage.setItem(`commentCollapse:${item.id}`, 'nope')
|
||||||
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
|
}}
|
||||||
extraInfo={
|
>reply from someone you muted
|
||||||
<>
|
</span>)
|
||||||
{includeParent && <Parent item={item} rootText={rootText} />}
|
: <ItemInfo
|
||||||
{bountyPaid &&
|
item={item}
|
||||||
<ActionTooltip notForm overlayText={`${numWithUnits(root.bounty)} paid`}>
|
pendingSats={pendingSats}
|
||||||
<BountyIcon className={`${styles.bountyIcon} ${'fill-success vertical-align-middle'}`} height={16} width={16} />
|
commentsText='replies'
|
||||||
</ActionTooltip>}
|
commentTextSingular='reply'
|
||||||
</>
|
className={`${itemStyles.other} ${styles.other}`}
|
||||||
}
|
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
|
||||||
onEdit={e => { setEdit(!edit) }}
|
extraInfo={
|
||||||
editText={edit ? 'cancel' : 'edit'}
|
<>
|
||||||
/>
|
{includeParent && <Parent item={item} rootText={rootText} />}
|
||||||
|
{bountyPaid &&
|
||||||
|
<ActionTooltip notForm overlayText={`${numWithUnits(root.bounty)} paid`}>
|
||||||
|
<BountyIcon className={`${styles.bountyIcon} ${'fill-success vertical-align-middle'}`} height={16} width={16} />
|
||||||
|
</ActionTooltip>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onEdit={e => { setEdit(!edit) }}
|
||||||
|
editText={edit ? 'cancel' : 'edit'}
|
||||||
|
/>}
|
||||||
|
|
||||||
{!includeParent && (collapse === 'yep'
|
{!includeParent && (collapse === 'yep'
|
||||||
? <Eye
|
? <Eye
|
||||||
className={styles.collapser} height={10} width={10} onClick={() => {
|
className={styles.collapser} height={10} width={10} onClick={() => {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { CopyLinkDropdownItem } from './share'
|
||||||
import Hat from './hat'
|
import Hat from './hat'
|
||||||
import { AD_USER_ID } from '../lib/constants'
|
import { AD_USER_ID } from '../lib/constants'
|
||||||
import ActionDropdown from './action-dropdown'
|
import ActionDropdown from './action-dropdown'
|
||||||
|
import MuteDropdownItem from './mute'
|
||||||
|
|
||||||
export default function ItemInfo ({
|
export default function ItemInfo ({
|
||||||
item, pendingSats, full, commentsText = 'comments',
|
item, pendingSats, full, commentsText = 'comments',
|
||||||
|
@ -131,15 +132,20 @@ export default function ItemInfo ({
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<CopyLinkDropdownItem item={item} />
|
<CopyLinkDropdownItem item={item} />
|
||||||
{me && <BookmarkDropdownItem item={item} />}
|
{me && <BookmarkDropdownItem item={item} />}
|
||||||
{me && item.user.id !== me.id && <SubscribeDropdownItem item={item} />}
|
{me && !item.mine && <SubscribeDropdownItem item={item} />}
|
||||||
{item.otsHash &&
|
{item.otsHash &&
|
||||||
<Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
|
<Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
|
||||||
ots timestamp
|
opentimestamp
|
||||||
</Link>}
|
</Link>}
|
||||||
{me && !item.meSats && !item.position &&
|
{me && !item.meSats && !item.position &&
|
||||||
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />}
|
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />}
|
||||||
{item.mine && !item.position && !item.deletedAt &&
|
{item.mine && !item.position && !item.deletedAt &&
|
||||||
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />}
|
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />}
|
||||||
|
{me && !item.mine &&
|
||||||
|
<>
|
||||||
|
<hr className='dropdown-divider' />
|
||||||
|
<MuteDropdownItem user={item.user} />
|
||||||
|
</>}
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
{extraInfo}
|
{extraInfo}
|
||||||
</div>
|
</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 SubscribeUserDropdownItem from './subscribeUser'
|
||||||
import ActionDropdown from './action-dropdown'
|
import ActionDropdown from './action-dropdown'
|
||||||
import CodeIcon from '../svgs/terminal-box-fill.svg'
|
import CodeIcon from '../svgs/terminal-box-fill.svg'
|
||||||
|
import MuteDropdownItem from './mute'
|
||||||
|
|
||||||
export default function UserHeader ({ user }) {
|
export default function UserHeader ({ user }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -156,11 +157,12 @@ function NymView ({ user, isMe, setEditting }) {
|
||||||
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
||||||
{isMe &&
|
{isMe &&
|
||||||
<Button className='py-0' style={{ lineHeight: '1.25' }} variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
|
<Button className='py-0' style={{ lineHeight: '1.25' }} variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
|
||||||
{!isMe &&
|
{!isMe && me &&
|
||||||
<div className='ms-2'>
|
<div className='ms-2'>
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
{me && <SubscribeUserDropdownItem user={user} target='posts' />}
|
<SubscribeUserDropdownItem user={user} target='posts' />
|
||||||
{me && <SubscribeUserDropdownItem user={user} target='comments' />}
|
<SubscribeUserDropdownItem user={user} target='comments' />
|
||||||
|
<MuteDropdownItem user={user} />
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const COMMENT_FIELDS = gql`
|
||||||
streak
|
streak
|
||||||
hideCowboyHat
|
hideCowboyHat
|
||||||
id
|
id
|
||||||
|
meMute
|
||||||
}
|
}
|
||||||
sats
|
sats
|
||||||
meAnonSats @client
|
meAnonSats @client
|
||||||
|
|
|
@ -14,6 +14,7 @@ export const ITEM_FIELDS = gql`
|
||||||
streak
|
streak
|
||||||
hideCowboyHat
|
hideCowboyHat
|
||||||
id
|
id
|
||||||
|
meMute
|
||||||
}
|
}
|
||||||
otsHash
|
otsHash
|
||||||
position
|
position
|
||||||
|
|
|
@ -158,6 +158,7 @@ export const USER_FIELDS = gql`
|
||||||
isContributor
|
isContributor
|
||||||
meSubscriptionPosts
|
meSubscriptionPosts
|
||||||
meSubscriptionComments
|
meSubscriptionComments
|
||||||
|
meMute
|
||||||
}`
|
}`
|
||||||
|
|
||||||
export const TOP_USERS = gql`
|
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)
|
hideWelcomeBanner Boolean @default(false)
|
||||||
diagnostics Boolean @default(false)
|
diagnostics Boolean @default(false)
|
||||||
hideIsContributor Boolean @default(false)
|
hideIsContributor Boolean @default(false)
|
||||||
|
muters Mute[] @relation("muter")
|
||||||
|
muteds Mute[] @relation("muted")
|
||||||
|
|
||||||
@@index([createdAt], map: "users.created_at_index")
|
@@index([createdAt], map: "users.created_at_index")
|
||||||
@@index([inviteId], map: "users.inviteId_index")
|
@@index([inviteId], map: "users.inviteId_index")
|
||||||
@@map("users")
|
@@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 {
|
model Streak {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
@ -536,14 +550,14 @@ model ThreadSubscription {
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserSubscription {
|
model UserSubscription {
|
||||||
followerId Int
|
followerId Int
|
||||||
followeeId Int
|
followeeId Int
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
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?
|
postsSubscribedAt DateTime?
|
||||||
commentsSubscribedAt DateTime?
|
commentsSubscribedAt DateTime?
|
||||||
follower User @relation("follower", fields: [followerId], references: [id], onDelete: Cascade)
|
follower User @relation("follower", fields: [followerId], references: [id], onDelete: Cascade)
|
||||||
followee User @relation("followee", fields: [followeeId], references: [id], onDelete: Cascade)
|
followee User @relation("followee", fields: [followeeId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@id([followerId, followeeId])
|
@@id([followerId, followeeId])
|
||||||
@@index([createdAt], map: "UserSubscription.created_at_index")
|
@@index([createdAt], map: "UserSubscription.created_at_index")
|
||||||
|
@ -626,4 +640,4 @@ enum LogLevel {
|
||||||
INFO
|
INFO
|
||||||
WARN
|
WARN
|
||||||
ERROR
|
ERROR
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue