Proposal: User Subscriptions: separate posts and comments (#470)
* Subscribe to user posts and comments independently * Track when comments and posts subscriptions are set to filter out old items * Only send push notification to subscribed user if posts/comments enabled * Remove `posts` and `comments` boolean fields on UserSub, rely solely on timestamps
This commit is contained in:
parent
654ecaf00a
commit
8ab58fff87
@ -1203,15 +1203,16 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
|
|||||||
|
|
||||||
const notifyUserSubscribers = async () => {
|
const notifyUserSubscribers = async () => {
|
||||||
try {
|
try {
|
||||||
|
const isPost = !!item.title
|
||||||
const userSubs = await models.userSubscription.findMany({
|
const userSubs = await models.userSubscription.findMany({
|
||||||
where: {
|
where: {
|
||||||
followeeId: Number(item.userId)
|
followeeId: Number(item.userId),
|
||||||
|
[isPost ? 'postsSubscribedAt' : 'commentsSubscribedAt']: { not: null }
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
followee: true
|
followee: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const isPost = !!item.title
|
|
||||||
await Promise.allSettled(userSubs.map(({ followerId, followee }) => sendUserNotification(followerId, {
|
await Promise.allSettled(userSubs.map(({ followerId, followee }) => sendUserNotification(followerId, {
|
||||||
title: `@${followee.name} ${isPost ? 'created a post' : 'replied to a post'}`,
|
title: `@${followee.name} ${isPost ? 'created a post' : 'replied to a post'}`,
|
||||||
body: isPost ? item.title : item.text,
|
body: isPost ? item.title : item.text,
|
||||||
|
@ -112,8 +112,11 @@ export default {
|
|||||||
JOIN "UserSubscription" ON "Item"."userId" = "UserSubscription"."followeeId"
|
JOIN "UserSubscription" ON "Item"."userId" = "UserSubscription"."followeeId"
|
||||||
WHERE "UserSubscription"."followerId" = $1
|
WHERE "UserSubscription"."followerId" = $1
|
||||||
AND "Item".created_at <= $2
|
AND "Item".created_at <= $2
|
||||||
-- Only show items that have been created since subscribing to the user
|
AND (
|
||||||
AND "Item".created_at >= "UserSubscription".created_at
|
-- 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")
|
||||||
|
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)}
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT}+$3`
|
LIMIT ${LIMIT}+$3`
|
||||||
|
@ -323,6 +323,10 @@ export default {
|
|||||||
WHERE
|
WHERE
|
||||||
"UserSubscription"."followerId" = $1
|
"UserSubscription"."followerId" = $1
|
||||||
AND "Item".created_at > $2::timestamp(3) without time zone
|
AND "Item".created_at > $2::timestamp(3) without time zone
|
||||||
|
AND (
|
||||||
|
("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
|
||||||
|
OR ("Item"."parentId" IS NOT NULL AND "UserSubscription"."commentsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."commentsSubscribedAt")
|
||||||
|
)
|
||||||
${await filterClause(me, models)}
|
${await filterClause(me, models)}
|
||||||
LIMIT 1`, me.id, lastChecked)
|
LIMIT 1`, me.id, lastChecked)
|
||||||
if (newUserSubs.length > 0) {
|
if (newUserSubs.length > 0) {
|
||||||
@ -588,13 +592,23 @@ export default {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
subscribeUser: async (parent, { id }, { me, models }) => {
|
subscribeUserPosts: async (parent, { id }, { me, models }) => {
|
||||||
const data = { followerId: Number(me.id), followeeId: Number(id) }
|
const lookupData = { followerId: Number(me.id), followeeId: Number(id) }
|
||||||
const old = await models.userSubscription.findUnique({ where: { followerId_followeeId: data } })
|
const existing = await models.userSubscription.findUnique({ where: { followerId_followeeId: lookupData } })
|
||||||
if (old) {
|
if (existing) {
|
||||||
await models.userSubscription.delete({ where: { followerId_followeeId: data } })
|
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { postsSubscribedAt: existing.postsSubscribedAt ? null : new Date() } })
|
||||||
} else {
|
} else {
|
||||||
await models.userSubscription.create({ data })
|
await models.userSubscription.create({ data: { ...lookupData, postsSubscribedAt: new Date() } })
|
||||||
|
}
|
||||||
|
return { id }
|
||||||
|
},
|
||||||
|
subscribeUserComments: async (parent, { id }, { me, models }) => {
|
||||||
|
const lookupData = { followerId: Number(me.id), followeeId: Number(id) }
|
||||||
|
const existing = await models.userSubscription.findUnique({ where: { followerId_followeeId: lookupData } })
|
||||||
|
if (existing) {
|
||||||
|
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { commentsSubscribedAt: existing.commentsSubscribedAt ? null : new Date() } })
|
||||||
|
} else {
|
||||||
|
await models.userSubscription.create({ data: { ...lookupData, commentsSubscribedAt: new Date() } })
|
||||||
}
|
}
|
||||||
return { id }
|
return { id }
|
||||||
},
|
},
|
||||||
@ -774,9 +788,9 @@ export default {
|
|||||||
|
|
||||||
return relays?.map(r => r.nostrRelayAddr)
|
return relays?.map(r => r.nostrRelayAddr)
|
||||||
},
|
},
|
||||||
meSubscription: async (user, args, { me, models }) => {
|
meSubscriptionPosts: async (user, args, { me, models }) => {
|
||||||
if (!me) return false
|
if (!me) return false
|
||||||
if (typeof user.meSubscription !== 'undefined') return user.meSubscription
|
if (typeof user.meSubscriptionPosts !== 'undefined') return user.meSubscriptionPosts
|
||||||
|
|
||||||
const subscription = await models.userSubscription.findUnique({
|
const subscription = await models.userSubscription.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@ -787,7 +801,22 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return !!subscription
|
return !!subscription?.postsSubscribedAt
|
||||||
|
},
|
||||||
|
meSubscriptionComments: async (user, args, { me, models }) => {
|
||||||
|
if (!me) return false
|
||||||
|
if (typeof user.meSubscriptionComments !== 'undefined') return user.meSubscriptionComments
|
||||||
|
|
||||||
|
const subscription = await models.userSubscription.findUnique({
|
||||||
|
where: {
|
||||||
|
followerId_followeeId: {
|
||||||
|
followerId: Number(me.id),
|
||||||
|
followeeId: Number(user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return !!subscription?.commentsSubscribedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,9 @@ export default gql`
|
|||||||
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
|
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
|
||||||
unlinkAuth(authType: String!): AuthMethods!
|
unlinkAuth(authType: String!): AuthMethods!
|
||||||
linkUnverifiedEmail(email: String!): Boolean
|
linkUnverifiedEmail(email: String!): Boolean
|
||||||
subscribeUser(id: ID): User
|
|
||||||
hideWelcomeBanner: Boolean
|
hideWelcomeBanner: Boolean
|
||||||
|
subscribeUserPosts(id: ID): User
|
||||||
|
subscribeUserComments(id: ID): User
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthMethods {
|
type AuthMethods {
|
||||||
@ -91,6 +92,7 @@ export default gql`
|
|||||||
greeterMode: Boolean!
|
greeterMode: Boolean!
|
||||||
lastCheckedJobs: String
|
lastCheckedJobs: String
|
||||||
authMethods: AuthMethods!
|
authMethods: AuthMethods!
|
||||||
meSubscription: Boolean!
|
meSubscriptionPosts: Boolean!
|
||||||
|
meSubscriptionComments: Boolean!
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -3,20 +3,24 @@ import { gql } from 'graphql-tag'
|
|||||||
import Dropdown from 'react-bootstrap/Dropdown'
|
import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
|
|
||||||
export default function SubscribeUserDropdownItem ({ user: { id, meSubscription } }) {
|
export default function SubscribeUserDropdownItem ({ user, target = 'posts' }) {
|
||||||
|
const isPosts = target === 'posts'
|
||||||
|
const mutation = isPosts ? 'subscribeUserPosts' : 'subscribeUserComments'
|
||||||
|
const userField = isPosts ? 'meSubscriptionPosts' : 'meSubscriptionComments'
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
const { id, [userField]: meSubscription } = user
|
||||||
const [subscribeUser] = useMutation(
|
const [subscribeUser] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation subscribeUser($id: ID!) {
|
mutation ${mutation}($id: ID!) {
|
||||||
subscribeUser(id: $id) {
|
${mutation}(id: $id) {
|
||||||
meSubscription
|
${userField}
|
||||||
}
|
}
|
||||||
}`, {
|
}`, {
|
||||||
update (cache, { data: { subscribeUser } }) {
|
update (cache, { data: { [mutation]: subscribeUser } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: `User:${id}`,
|
id: `User:${id}`,
|
||||||
fields: {
|
fields: {
|
||||||
meSubscription: () => subscribeUser.meSubscription
|
[userField]: () => subscribeUser[userField]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -34,7 +38,9 @@ export default function SubscribeUserDropdownItem ({ user: { id, meSubscription
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{meSubscription ? 'remove subscription' : 'subscribe'}
|
{meSubscription
|
||||||
|
? `unsubscribe from ${isPosts ? 'posts' : 'comments'}`
|
||||||
|
: `subscribe to ${isPosts ? 'posts' : 'comments'}`}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,8 @@ function NymView ({ user, isMe, setEditting }) {
|
|||||||
{!isMe &&
|
{!isMe &&
|
||||||
<div className='ms-2'>
|
<div className='ms-2'>
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
{me && <SubscribeUserDropdownItem user={user} />}
|
{me && <SubscribeUserDropdownItem user={user} target='posts' />}
|
||||||
|
{me && <SubscribeUserDropdownItem user={user} target='comments' />}
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -150,7 +150,8 @@ export const USER_FIELDS = gql`
|
|||||||
stacked
|
stacked
|
||||||
since
|
since
|
||||||
photoId
|
photoId
|
||||||
meSubscription
|
meSubscriptionPosts
|
||||||
|
meSubscriptionComments
|
||||||
}`
|
}`
|
||||||
|
|
||||||
export const TOP_USERS = gql`
|
export const TOP_USERS = gql`
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UserSubscription"
|
||||||
|
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ADD COLUMN "commentsSubscribedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "postsSubscribedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- Set the individual post and comment times based on the original creation time for pre-existing subscriptions
|
||||||
|
UPDATE "UserSubscription"
|
||||||
|
SET "commentsSubscribedAt" = "UserSubscription".created_at,
|
||||||
|
"postsSubscribedAt" = "UserSubscription".created_at;
|
@ -533,11 +533,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")
|
||||||
follower User @relation("follower", fields: [followerId], references: [id], onDelete: Cascade)
|
updatedAt DateTime @default(now()) @map("updated_at") @updatedAt
|
||||||
followee User @relation("followee", fields: [followeeId], references: [id], onDelete: Cascade)
|
postsSubscribedAt DateTime?
|
||||||
|
commentsSubscribedAt DateTime?
|
||||||
|
follower User @relation("follower", fields: [followerId], 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")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user