Add thread subscriptions (#293)
* Add thread subscriptions * remove dead code: reply only notifications * break out thread subscription queries to reduce search space * one db dip for item lists/threads re:meSubscription --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
parent
e97509eea7
commit
0c251ca376
|
@ -173,12 +173,14 @@ async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args)
|
||||||
} else {
|
} else {
|
||||||
return await models.$queryRaw(`
|
return await models.$queryRaw(`
|
||||||
SELECT "Item".*, to_json(users.*) as user, COALESCE("ItemAct"."meMsats", 0) as "meMsats",
|
SELECT "Item".*, to_json(users.*) as user, COALESCE("ItemAct"."meMsats", 0) as "meMsats",
|
||||||
COALESCE("ItemAct"."meDontLike", false) as "meDontLike", "Bookmark"."itemId" IS NOT NULL AS "meBookmark"
|
COALESCE("ItemAct"."meDontLike", false) as "meDontLike", "Bookmark"."itemId" IS NOT NULL AS "meBookmark",
|
||||||
|
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription"
|
||||||
FROM (
|
FROM (
|
||||||
${query}
|
${query}
|
||||||
) "Item"
|
) "Item"
|
||||||
JOIN users ON "Item"."userId" = users.id
|
JOIN users ON "Item"."userId" = users.id
|
||||||
LEFT JOIN "Bookmark" ON "Bookmark"."itemId" = "Item".id AND "Bookmark"."userId" = ${me.id}
|
LEFT JOIN "Bookmark" ON "Bookmark"."itemId" = "Item".id AND "Bookmark"."userId" = ${me.id}
|
||||||
|
LEFT JOIN "ThreadSubscription" ON "ThreadSubscription"."itemId" = "Item".id AND "ThreadSubscription"."userId" = ${me.id}
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT "itemId", sum("ItemAct".msats) FILTER (WHERE act = 'FEE' OR act = 'TIP') AS "meMsats",
|
SELECT "itemId", sum("ItemAct".msats) FILTER (WHERE act = 'FEE' OR act = 'TIP') AS "meMsats",
|
||||||
bool_or(act = 'DONT_LIKE_THIS') AS "meDontLike"
|
bool_or(act = 'DONT_LIKE_THIS') AS "meDontLike"
|
||||||
|
@ -719,6 +721,14 @@ export default {
|
||||||
} else await models.bookmark.create({ data })
|
} else await models.bookmark.create({ data })
|
||||||
return { id }
|
return { id }
|
||||||
},
|
},
|
||||||
|
subscribeItem: async (parent, { id }, { me, models }) => {
|
||||||
|
const data = { itemId: Number(id), userId: me.id }
|
||||||
|
const old = await models.threadSubscription.findUnique({ where: { userId_itemId: data } })
|
||||||
|
if (old) {
|
||||||
|
await models.threadSubscription.delete({ where: { userId_itemId: data } })
|
||||||
|
} else await models.threadSubscription.create({ data })
|
||||||
|
return { id }
|
||||||
|
},
|
||||||
deleteItem: async (parent, { id }, { me, models }) => {
|
deleteItem: async (parent, { id }, { me, models }) => {
|
||||||
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
||||||
if (Number(old.userId) !== Number(me?.id)) {
|
if (Number(old.userId) !== Number(me?.id)) {
|
||||||
|
@ -1067,6 +1077,21 @@ export default {
|
||||||
|
|
||||||
return !!bookmark
|
return !!bookmark
|
||||||
},
|
},
|
||||||
|
meSubscription: async (item, args, { me, models }) => {
|
||||||
|
if (!me) return false
|
||||||
|
if (typeof item.meSubscription === 'boolean') return item.meSubscription
|
||||||
|
|
||||||
|
const subscription = await models.threadSubscription.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_itemId: {
|
||||||
|
itemId: Number(item.id),
|
||||||
|
userId: me.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return !!subscription
|
||||||
|
},
|
||||||
outlawed: async (item, args, { me, models }) => {
|
outlawed: async (item, args, { me, models }) => {
|
||||||
if (me && Number(item.userId) === Number(me.id)) {
|
if (me && Number(item.userId) === Number(me.id)) {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -69,127 +69,129 @@ export default {
|
||||||
|
|
||||||
const queries = []
|
const queries = []
|
||||||
|
|
||||||
if (inc === 'replies') {
|
queries.push(
|
||||||
queries.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",
|
'Reply' AS type
|
||||||
'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)}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
queries.push(
|
|
||||||
`(SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
|
|
||||||
'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)}
|
|
||||||
ORDER BY "sortTime" DESC
|
|
||||||
LIMIT ${LIMIT}+$3)`
|
|
||||||
)
|
|
||||||
|
|
||||||
queries.push(
|
|
||||||
`(SELECT "Item".id::text, "Item"."statusUpdatedAt" AS "sortTime", NULL as "earnedSats",
|
|
||||||
'JobChanged' AS type
|
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
WHERE "Item"."userId" = $1
|
JOIN "Item" p ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
||||||
AND "maxBid" IS NOT NULL
|
WHERE p."userId" = $1 AND "Item"."userId" <> $1 AND "Item".created_at <= $2
|
||||||
AND "statusUpdatedAt" <= $2 AND "statusUpdatedAt" <> created_at
|
${await filterClause(me, models)}
|
||||||
|
ORDER BY "sortTime" DESC
|
||||||
|
LIMIT ${LIMIT}+$3)`
|
||||||
|
)
|
||||||
|
|
||||||
|
// break out thread subscription to decrease the search space of the already expensive reply query
|
||||||
|
queries.push(
|
||||||
|
`(SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
|
||||||
|
'Reply' AS type
|
||||||
|
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
|
||||||
|
${await filterClause(me, models)}
|
||||||
|
ORDER BY "sortTime" DESC
|
||||||
|
LIMIT ${LIMIT}+$3)`
|
||||||
|
)
|
||||||
|
|
||||||
|
queries.push(
|
||||||
|
`(SELECT "Item".id::text, "Item"."statusUpdatedAt" AS "sortTime", NULL as "earnedSats",
|
||||||
|
'JobChanged' AS type
|
||||||
|
FROM "Item"
|
||||||
|
WHERE "Item"."userId" = $1
|
||||||
|
AND "maxBid" IS NOT NULL
|
||||||
|
AND "statusUpdatedAt" <= $2 AND "statusUpdatedAt" <> created_at
|
||||||
|
ORDER BY "sortTime" DESC
|
||||||
|
LIMIT ${LIMIT}+$3)`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (meFull.noteItemSats) {
|
||||||
|
queries.push(
|
||||||
|
`(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
|
||||||
|
MAX("Item".msats/1000) as "earnedSats", 'Votification' AS type
|
||||||
|
FROM "Item"
|
||||||
|
JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id
|
||||||
|
WHERE "ItemAct"."userId" <> $1
|
||||||
|
AND "ItemAct".created_at <= $2
|
||||||
|
AND "ItemAct".act IN ('TIP', 'FEE')
|
||||||
|
AND "Item"."userId" = $1
|
||||||
|
GROUP BY "Item".id
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT}+$3)`
|
LIMIT ${LIMIT}+$3)`
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (meFull.noteItemSats) {
|
if (meFull.noteMentions) {
|
||||||
queries.push(
|
queries.push(
|
||||||
`(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
|
`(SELECT "Item".id::TEXT, "Mention".created_at AS "sortTime", NULL as "earnedSats",
|
||||||
MAX("Item".msats/1000) as "earnedSats", 'Votification' AS type
|
'Mention' AS type
|
||||||
FROM "Item"
|
FROM "Mention"
|
||||||
JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id
|
JOIN "Item" ON "Mention"."itemId" = "Item".id
|
||||||
WHERE "ItemAct"."userId" <> $1
|
LEFT JOIN "Item" p ON "Item"."parentId" = p.id
|
||||||
AND "ItemAct".created_at <= $2
|
WHERE "Mention"."userId" = $1
|
||||||
AND "ItemAct".act IN ('TIP', 'FEE')
|
AND "Mention".created_at <= $2
|
||||||
AND "Item"."userId" = $1
|
AND "Item"."userId" <> $1
|
||||||
GROUP BY "Item".id
|
AND (p."userId" IS NULL OR p."userId" <> $1)
|
||||||
ORDER BY "sortTime" DESC
|
${await filterClause(me, models)}
|
||||||
LIMIT ${LIMIT}+$3)`
|
ORDER BY "sortTime" DESC
|
||||||
)
|
LIMIT ${LIMIT}+$3)`
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (meFull.noteMentions) {
|
if (meFull.noteDeposits) {
|
||||||
queries.push(
|
queries.push(
|
||||||
`(SELECT "Item".id::TEXT, "Mention".created_at AS "sortTime", NULL as "earnedSats",
|
`(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime", FLOOR("msatsReceived" / 1000) as "earnedSats",
|
||||||
'Mention' AS type
|
'InvoicePaid' AS type
|
||||||
FROM "Mention"
|
FROM "Invoice"
|
||||||
JOIN "Item" ON "Mention"."itemId" = "Item".id
|
WHERE "Invoice"."userId" = $1
|
||||||
LEFT JOIN "Item" p ON "Item"."parentId" = p.id
|
AND "confirmedAt" IS NOT NULL
|
||||||
WHERE "Mention"."userId" = $1
|
|
||||||
AND "Mention".created_at <= $2
|
|
||||||
AND "Item"."userId" <> $1
|
|
||||||
AND (p."userId" IS NULL OR p."userId" <> $1)
|
|
||||||
${await filterClause(me, models)}
|
|
||||||
ORDER BY "sortTime" DESC
|
|
||||||
LIMIT ${LIMIT}+$3)`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meFull.noteDeposits) {
|
|
||||||
queries.push(
|
|
||||||
`(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime", FLOOR("msatsReceived" / 1000) as "earnedSats",
|
|
||||||
'InvoicePaid' AS type
|
|
||||||
FROM "Invoice"
|
|
||||||
WHERE "Invoice"."userId" = $1
|
|
||||||
AND "confirmedAt" IS NOT NULL
|
|
||||||
AND created_at <= $2
|
|
||||||
ORDER BY "sortTime" DESC
|
|
||||||
LIMIT ${LIMIT}+$3)`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meFull.noteInvites) {
|
|
||||||
queries.push(
|
|
||||||
`(SELECT "Invite".id, MAX(users.created_at) AS "sortTime", NULL as "earnedSats",
|
|
||||||
'Invitification' AS type
|
|
||||||
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
|
|
||||||
WHERE "Invite"."userId" = $1
|
|
||||||
AND users.created_at <= $2
|
|
||||||
GROUP BY "Invite".id
|
|
||||||
ORDER BY "sortTime" DESC
|
|
||||||
LIMIT ${LIMIT}+$3)`
|
|
||||||
)
|
|
||||||
queries.push(
|
|
||||||
`(SELECT users.id::text, users.created_at AS "sortTime", NULL as "earnedSats",
|
|
||||||
'Referral' AS type
|
|
||||||
FROM users
|
|
||||||
WHERE "users"."referrerId" = $1
|
|
||||||
AND "inviteId" IS NULL
|
|
||||||
AND users.created_at <= $2
|
|
||||||
LIMIT ${LIMIT}+$3)`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meFull.noteEarning) {
|
|
||||||
queries.push(
|
|
||||||
`SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats",
|
|
||||||
'Earn' AS type
|
|
||||||
FROM "Earn"
|
|
||||||
WHERE "userId" = $1
|
|
||||||
AND created_at <= $2
|
AND created_at <= $2
|
||||||
GROUP BY "userId", created_at`
|
ORDER BY "sortTime" DESC
|
||||||
)
|
LIMIT ${LIMIT}+$3)`
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (meFull.noteCowboyHat) {
|
if (meFull.noteInvites) {
|
||||||
queries.push(
|
queries.push(
|
||||||
`SELECT id::text, updated_at AS "sortTime", 0 as "earnedSats", 'Streak' AS type
|
`(SELECT "Invite".id, MAX(users.created_at) AS "sortTime", NULL as "earnedSats",
|
||||||
FROM "Streak"
|
'Invitification' AS type
|
||||||
WHERE "userId" = $1
|
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
|
||||||
AND updated_at <= $2`
|
WHERE "Invite"."userId" = $1
|
||||||
)
|
AND users.created_at <= $2
|
||||||
}
|
GROUP BY "Invite".id
|
||||||
|
ORDER BY "sortTime" DESC
|
||||||
|
LIMIT ${LIMIT}+$3)`
|
||||||
|
)
|
||||||
|
queries.push(
|
||||||
|
`(SELECT users.id::text, users.created_at AS "sortTime", NULL as "earnedSats",
|
||||||
|
'Referral' AS type
|
||||||
|
FROM users
|
||||||
|
WHERE "users"."referrerId" = $1
|
||||||
|
AND "inviteId" IS NULL
|
||||||
|
AND users.created_at <= $2
|
||||||
|
LIMIT ${LIMIT}+$3)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meFull.noteEarning) {
|
||||||
|
queries.push(
|
||||||
|
`SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats",
|
||||||
|
'Earn' AS type
|
||||||
|
FROM "Earn"
|
||||||
|
WHERE "userId" = $1
|
||||||
|
AND created_at <= $2
|
||||||
|
GROUP BY "userId", created_at`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meFull.noteCowboyHat) {
|
||||||
|
queries.push(
|
||||||
|
`SELECT id::text, updated_at AS "sortTime", 0 as "earnedSats", 'Streak' AS type
|
||||||
|
FROM "Streak"
|
||||||
|
WHERE "userId" = $1
|
||||||
|
AND updated_at <= $2`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// we do all this crazy subquery stuff to make 'reward' islands
|
// we do all this crazy subquery stuff to make 'reward' islands
|
||||||
|
|
|
@ -291,8 +291,8 @@ export default {
|
||||||
JOIN "Item" p ON
|
JOIN "Item" p ON
|
||||||
"Item".created_at >= p.created_at
|
"Item".created_at >= p.created_at
|
||||||
AND ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
AND ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
||||||
AND "Item"."userId" <> p."userId"
|
AND "Item"."userId" <> $1
|
||||||
WHERE p."userId" = $1
|
WHERE (p."userId" = $1 OR p.id = ANY(SELECT "itemId" FROM "ThreadSubscription" WHERE "userId" = $1))
|
||||||
AND "Item".created_at > $2::timestamp(3) without time zone
|
AND "Item".created_at > $2::timestamp(3) without time zone
|
||||||
${await filterClause(me, models)}
|
${await filterClause(me, models)}
|
||||||
LIMIT 1`, me.id, lastChecked)
|
LIMIT 1`, me.id, lastChecked)
|
||||||
|
@ -300,6 +300,21 @@ export default {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// break out thread subscription to decrease the search space of the already expensive reply query
|
||||||
|
const newtsubs = await models.$queryRaw(`
|
||||||
|
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) {
|
||||||
|
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.$queryRaw(`
|
const newMentions = await models.$queryRaw(`
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default gql`
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
bookmarkItem(id: ID): Item
|
bookmarkItem(id: ID): Item
|
||||||
|
subscribeItem(id: ID): Item
|
||||||
deleteItem(id: ID): Item
|
deleteItem(id: ID): Item
|
||||||
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String): Item!
|
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String): Item!
|
||||||
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String): Item!
|
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String): Item!
|
||||||
|
@ -102,6 +103,7 @@ export default gql`
|
||||||
meSats: Int!
|
meSats: Int!
|
||||||
meDontLike: Boolean!
|
meDontLike: Boolean!
|
||||||
meBookmark: Boolean!
|
meBookmark: Boolean!
|
||||||
|
meSubscription: Boolean!
|
||||||
outlawed: Boolean!
|
outlawed: Boolean!
|
||||||
freebie: Boolean!
|
freebie: Boolean!
|
||||||
paidImgLink: Boolean
|
paidImgLink: Boolean
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { useMe } from './me'
|
||||||
import MoreIcon from '../svgs/more-fill.svg'
|
import MoreIcon from '../svgs/more-fill.svg'
|
||||||
import DontLikeThisDropdownItem from './dont-link-this'
|
import DontLikeThisDropdownItem from './dont-link-this'
|
||||||
import BookmarkDropdownItem from './bookmark'
|
import BookmarkDropdownItem from './bookmark'
|
||||||
|
import SubscribeDropdownItem from './subscribe'
|
||||||
import { CopyLinkDropdownItem } from './share'
|
import { CopyLinkDropdownItem } from './share'
|
||||||
|
|
||||||
export default function ItemInfo ({ item, full, commentsText, className, embellishUser, extraInfo, onEdit, editText }) {
|
export default function ItemInfo ({ item, full, commentsText, className, embellishUser, extraInfo, onEdit, editText }) {
|
||||||
|
@ -98,6 +99,7 @@ export default function ItemInfo ({ item, full, commentsText, className, embelli
|
||||||
<ItemDropdown>
|
<ItemDropdown>
|
||||||
<CopyLinkDropdownItem item={item} />
|
<CopyLinkDropdownItem item={item} />
|
||||||
{me && <BookmarkDropdownItem item={item} />}
|
{me && <BookmarkDropdownItem item={item} />}
|
||||||
|
{me && item.user.id !== me.id && <SubscribeDropdownItem item={item} />}
|
||||||
{item.otsHash &&
|
{item.otsHash &&
|
||||||
<Dropdown.Item>
|
<Dropdown.Item>
|
||||||
<Link passHref href={`/items/${item.id}/ots`}>
|
<Link passHref href={`/items/${item.id}/ots`}>
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useMutation } from '@apollo/client'
|
||||||
|
import { gql } from 'apollo-server-micro'
|
||||||
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
|
||||||
|
export default function SubscribeDropdownItem ({ item: { id, meSubscription } }) {
|
||||||
|
const [subscribeItem] = useMutation(
|
||||||
|
gql`
|
||||||
|
mutation subscribeItem($id: ID!) {
|
||||||
|
subscribeItem(id: $id) {
|
||||||
|
meSubscription
|
||||||
|
}
|
||||||
|
}`, {
|
||||||
|
update (cache, { data: { subscribeItem } }) {
|
||||||
|
cache.modify({
|
||||||
|
id: `Item:${id}`,
|
||||||
|
fields: {
|
||||||
|
meSubscription: () => subscribeItem.meSubscription
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={() => subscribeItem({ variables: { id } })}
|
||||||
|
>
|
||||||
|
{meSubscription ? 'remove subscription' : 'subscribe'}
|
||||||
|
</Dropdown.Item>
|
||||||
|
)
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ export const COMMENT_FIELDS = gql`
|
||||||
meSats
|
meSats
|
||||||
meDontLike
|
meDontLike
|
||||||
meBookmark
|
meBookmark
|
||||||
|
meSubscription
|
||||||
outlawed
|
outlawed
|
||||||
freebie
|
freebie
|
||||||
path
|
path
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const ITEM_FIELDS = gql`
|
||||||
meSats
|
meSats
|
||||||
meDontLike
|
meDontLike
|
||||||
meBookmark
|
meBookmark
|
||||||
|
meSubscription
|
||||||
outlawed
|
outlawed
|
||||||
freebie
|
freebie
|
||||||
ncomments
|
ncomments
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ThreadSubscription" (
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"itemId" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY ("userId","itemId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ThreadSubscription" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ThreadSubscription" ADD FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ThreadSubscription.created_at_index" ON "ThreadSubscription"("created_at");
|
|
@ -0,0 +1,38 @@
|
||||||
|
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.*) 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 "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
|
||||||
|
$$;
|
|
@ -85,14 +85,15 @@ model User {
|
||||||
wildWestMode Boolean @default(false)
|
wildWestMode Boolean @default(false)
|
||||||
greeterMode Boolean @default(false)
|
greeterMode Boolean @default(false)
|
||||||
|
|
||||||
Earn Earn[]
|
Earn Earn[]
|
||||||
Upload Upload[] @relation(name: "Uploads")
|
Upload Upload[] @relation(name: "Uploads")
|
||||||
PollVote PollVote[]
|
PollVote PollVote[]
|
||||||
Donation Donation[]
|
Donation Donation[]
|
||||||
ReferralAct ReferralAct[]
|
ReferralAct ReferralAct[]
|
||||||
Streak Streak[]
|
Streak Streak[]
|
||||||
Bookmarks Bookmark[]
|
Bookmarks Bookmark[]
|
||||||
Subscriptions Subscription[]
|
Subscriptions Subscription[]
|
||||||
|
ThreadSubscriptions ThreadSubscription[]
|
||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([inviteId])
|
@@index([inviteId])
|
||||||
|
@ -300,10 +301,11 @@ model Item {
|
||||||
// fields for polls
|
// fields for polls
|
||||||
pollCost Int?
|
pollCost Int?
|
||||||
|
|
||||||
User User[]
|
User User[]
|
||||||
PollOption PollOption[]
|
PollOption PollOption[]
|
||||||
PollVote PollVote[]
|
PollVote PollVote[]
|
||||||
Bookmark Bookmark[]
|
Bookmark Bookmark[]
|
||||||
|
ThreadSubscription ThreadSubscription[]
|
||||||
|
|
||||||
@@index([weightedVotes])
|
@@index([weightedVotes])
|
||||||
@@index([weightedDownVotes])
|
@@index([weightedDownVotes])
|
||||||
|
@ -561,3 +563,14 @@ model Bookmark {
|
||||||
@@id([userId, itemId])
|
@@id([userId, itemId])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ThreadSubscription {
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int
|
||||||
|
item Item @relation(fields: [itemId], references: [id])
|
||||||
|
itemId Int
|
||||||
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
|
|
||||||
|
@@id([userId, itemId])
|
||||||
|
@@index([createdAt])
|
||||||
|
}
|
Loading…
Reference in New Issue