From 6b19b10bb2e311270b379da613955eaa1cebd973 Mon Sep 17 00:00:00 2001 From: keyan Date: Wed, 19 Jan 2022 15:02:38 -0600 Subject: [PATCH] invite notifications --- api/resolvers/notifications.js | 67 ++++++++++++++++++++++------------ api/resolvers/user.js | 49 +++++++++++++++---------- api/typeDefs/notifications.js | 7 +++- components/invite.js | 52 ++++++++++++++++++++++++++ components/notifications.js | 57 ++++++++++++++++++++--------- fragments/notifications.js | 8 ++++ pages/invites/index.js | 51 +------------------------- 7 files changed, 181 insertions(+), 110 deletions(-) create mode 100644 components/invite.js diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index 65084409..e1a88237 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -1,5 +1,6 @@ import { AuthenticationError } from 'apollo-server-micro' import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' +import { getItem } from './item' export default { Query: { @@ -62,48 +63,50 @@ export default { // HACK to make notifications faster, we only return a limited sub set of the unioned // queries ... we only ever need at most LIMIT+current offset in the child queries to // have enough items to return in the union - let notifications = await models.$queryRaw(` - (SELECT ${ITEM_FIELDS}, "Item".created_at as "sortTime", NULL as "earnedSats", - false as mention + const notifications = await models.$queryRaw(` + (SELECT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL as "earnedSats", + 'Reply' AS type FROM "Item" JOIN "Item" p ON "Item"."parentId" = p.id WHERE p."userId" = $1 AND "Item"."userId" <> $1 AND "Item".created_at <= $2 - ORDER BY "Item".created_at desc + ORDER BY "Item".created_at DESC LIMIT ${LIMIT}+$3) UNION ALL - (SELECT ${ITEM_FIELDS}, max("ItemAct".created_at) as "sortTime", - sum("ItemAct".sats) as "earnedSats", false as mention + (SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime", + sum("ItemAct".sats) as "earnedSats", 'Votification' AS type FROM "Item" - JOIN "ItemAct" on "ItemAct"."itemId" = "Item".id + JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id WHERE "ItemAct"."userId" <> $1 AND "ItemAct".created_at <= $2 AND "ItemAct".act <> 'BOOST' AND "Item"."userId" = $1 GROUP BY ${ITEM_GROUP_FIELDS} - ORDER BY max("ItemAct".created_at) desc + ORDER BY MAX("ItemAct".created_at) DESC LIMIT ${LIMIT}+$3) UNION ALL - (SELECT ${ITEM_FIELDS}, "Mention".created_at as "sortTime", NULL as "earnedSats", - true as mention + (SELECT "Item".id::TEXT, "Mention".created_at AS "sortTime", NULL as "earnedSats", + 'Mention' AS type FROM "Mention" - JOIN "Item" on "Mention"."itemId" = "Item".id - LEFT JOIN "Item" p on "Item"."parentId" = p.id + JOIN "Item" ON "Mention"."itemId" = "Item".id + LEFT JOIN "Item" p ON "Item"."parentId" = p.id WHERE "Mention"."userId" = $1 AND "Mention".created_at <= $2 AND "Item"."userId" <> $1 AND (p."userId" IS NULL OR p."userId" <> $1) - ORDER BY "Mention".created_at desc + ORDER BY "Mention".created_at DESC LIMIT ${LIMIT}+$3) + UNION ALL + (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 OFFSET $3 LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset) - notifications = notifications.map(n => { - n.item = { ...n } - return n - }) - const { checkedNotesAt } = await models.user.findUnique({ where: { id: me.id } }) if (decodedCursor.offset === 0) { await models.user.update({ where: { id: me.id }, data: { checkedNotesAt: new Date() } }) @@ -117,8 +120,26 @@ export default { } }, Notification: { - __resolveType: async (notification, args, { models }) => - notification.earnedSats ? 'Votification' : (notification.mention ? 'Mention' : 'Reply') + __resolveType: async (n, args, { models }) => n.type + }, + Votification: { + item: async (n, args, { models }) => getItem(n, { id: n.id }, { models }) + }, + Reply: { + item: async (n, args, { models }) => getItem(n, { id: n.id }, { models }) + }, + Mention: { + mention: async (n, args, { models }) => true, + item: async (n, args, { models }) => getItem(n, { id: n.id }, { models }) + }, + Invitification: { + invite: async (n, args, { models }) => { + return await models.invite.findUnique({ + where: { + id: n.id + } + }) + } } } @@ -130,6 +151,6 @@ const ITEM_GROUP_FIELDS = `"Item".id, "Item".created_at, "Item".updated_at, "Item".title, "Item".text, "Item".url, "Item"."userId", "Item"."parentId", ltree2text("Item"."path")` -const ITEM_FIELDS = - `"Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title, - "Item".text, "Item".url, "Item"."userId", "Item"."parentId", ltree2text("Item"."path") AS path` +// const ITEM_FIELDS = +// `"Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title, +// "Item".text, "Item".url, "Item"."userId", "Item"."parentId", ltree2text("Item"."path") AS path` diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 11cb90cd..1d01f7d8 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -160,13 +160,13 @@ export default { // check if any votes have been cast for them since checkedNotesAt const votes = await models.$queryRaw(` SELECT "ItemAct".id, "ItemAct".created_at - FROM "ItemAct" - JOIN "Item" on "ItemAct"."itemId" = "Item".id - WHERE "ItemAct"."userId" <> $1 - AND ("ItemAct".created_at > $2 OR $2 IS NULL) - AND "ItemAct".act <> 'BOOST' - AND "Item"."userId" = $1 - LIMIT 1`, user.id, user.checkedNotesAt) + FROM "ItemAct" + JOIN "Item" on "ItemAct"."itemId" = "Item".id + WHERE "ItemAct"."userId" <> $1 + AND ("ItemAct".created_at > $2 OR $2 IS NULL) + AND "ItemAct".act <> 'BOOST' + AND "Item"."userId" = $1 + LIMIT 1`, user.id, user.checkedNotesAt) if (votes.length > 0) { return true } @@ -174,11 +174,11 @@ export default { // check if they have any replies since checkedNotesAt const newReplies = await models.$queryRaw(` SELECT "Item".id, "Item".created_at - From "Item" - JOIN "Item" p ON "Item"."parentId" = p.id - WHERE p."userId" = $1 - AND ("Item".created_at > $2 OR $2 IS NULL) AND "Item"."userId" <> $1 - LIMIT 1`, user.id, user.checkedNotesAt) + FROM "Item" + JOIN "Item" p ON "Item"."parentId" = p.id + WHERE p."userId" = $1 + AND ("Item".created_at > $2 OR $2 IS NULL) AND "Item"."userId" <> $1 + LIMIT 1`, user.id, user.checkedNotesAt) if (newReplies.length > 0) { return true } @@ -186,13 +186,24 @@ export default { // check if they have any mentions since checkedNotesAt const newMentions = await models.$queryRaw(` SELECT "Item".id, "Item".created_at - From "Mention" - JOIN "Item" ON "Mention"."itemId" = "Item".id - WHERE "Mention"."userId" = $1 - AND ("Mention".created_at > $2 OR $2 IS NULL) - AND "Item"."userId" <> $1 - LIMIT 1`, user.id, user.checkedNotesAt) - return newMentions.length > 0 + FROM "Mention" + JOIN "Item" ON "Mention"."itemId" = "Item".id + WHERE "Mention"."userId" = $1 + AND ("Mention".created_at > $2 OR $2 IS NULL) + AND "Item"."userId" <> $1 + LIMIT 1`, user.id, user.checkedNotesAt) + if (newMentions.length > 0) { + return true + } + + // check if new invites have been redeemed + const newInvitees = await models.$queryRaw(` + SELECT "Invite".id + FROM users JOIN "Invite" on users."inviteId" = "Invite".id + WHERE "Invite"."userId" = $1 + AND (users.created_at > $2 or $2 IS NULL) + LIMIT 1`, user.id, user.checkedNotesAt) + return newInvitees.length > 0 } } } diff --git a/api/typeDefs/notifications.js b/api/typeDefs/notifications.js index 3071db4e..704ec4fb 100644 --- a/api/typeDefs/notifications.js +++ b/api/typeDefs/notifications.js @@ -22,7 +22,12 @@ export default gql` sortTime: String! } - union Notification = Reply | Votification | Mention + type Invitification { + invite: Invite! + sortTime: String! + } + + union Notification = Reply | Votification | Mention | Invitification type Notifications { lastChecked: String diff --git a/components/invite.js b/components/invite.js new file mode 100644 index 00000000..af9e5212 --- /dev/null +++ b/components/invite.js @@ -0,0 +1,52 @@ +import { CopyInput } from './form' +import { gql, useMutation } from '@apollo/client' +import { INVITE_FIELDS } from '../fragments/invites' +import styles from '../styles/invites.module.css' + +export default function Invite ({ invite, active }) { + const [revokeInvite] = useMutation( + gql` + ${INVITE_FIELDS} + mutation revokeInvite($id: ID!) { + revokeInvite(id: $id) { + ...InviteFields + } + }` + ) + + return ( +
+ +
+ {invite.gift} sat gift + \ + {invite.invitees.length} joined{invite.limit ? ` of ${invite.limit}` : ''} + {active + ? ( + <> + \ + revokeInvite({ variables: { id: invite.id } })} + >revoke + + ) + + : invite.revoked && ( + <> + \ + revoked + + )} +
+
+ ) +} diff --git a/components/notifications.js b/components/notifications.js index 20b1bbef..850237ae 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -4,6 +4,7 @@ import Item from './item' import { NOTIFICATIONS } from '../fragments/notifications' import { useRouter } from 'next/router' import MoreFooter from './more-footer' +import Invite from './invite' function Notification ({ n }) { const router = useRouter() @@ -11,7 +12,9 @@ function Notification ({ n }) {
{ - if (n.__typename === 'Reply' || !n.item.title) { + if (n.__typename === 'Invitification') { + router.push('/invites') + } else if (!n.item.title) { router.push({ pathname: '/items/[id]', query: { id: n.item.root.id, commentId: n.item.id } @@ -24,23 +27,41 @@ function Notification ({ n }) { } }} > - {n.__typename === 'Votification' && - your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats} - {n.__typename === 'Mention' && - you were mentioned in} -
- {n.item.title - ? - : ( -
- -
)} -
+ {n.__typename === 'Invitification' + ? ( + <> + + your invite has been redeemed by {n.invite.invitees.length} users + +
+ = n.invite.limit) + } + /> +
+ + ) + : ( + <> + {n.__typename === 'Votification' && + + your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats + } + {n.__typename === 'Mention' && + + you were mentioned in + } +
+ {n.item.title + ? + : ( +
+ +
)} +
+ )}
) } diff --git a/fragments/notifications.js b/fragments/notifications.js index f855e30f..08d4cf38 100644 --- a/fragments/notifications.js +++ b/fragments/notifications.js @@ -1,8 +1,10 @@ import { gql } from '@apollo/client' import { ITEM_FIELDS } from './items' +import { INVITE_FIELDS } from './invites' export const NOTIFICATIONS = gql` ${ITEM_FIELDS} + ${INVITE_FIELDS} query Notifications($cursor: String) { notifications(cursor: $cursor) { @@ -33,6 +35,12 @@ export const NOTIFICATIONS = gql` text } } + ... on Invitification { + sortTime + invite { + ...InviteFields + } + } } } } ` diff --git a/pages/invites/index.js b/pages/invites/index.js index 199a0278..9b580dac 100644 --- a/pages/invites/index.js +++ b/pages/invites/index.js @@ -1,11 +1,12 @@ import Layout from '../../components/layout' import * as Yup from 'yup' -import { CopyInput, Form, Input, SubmitButton } from '../../components/form' +import { Form, Input, SubmitButton } from '../../components/form' import { InputGroup } from 'react-bootstrap' import { gql, useMutation, useQuery } from '@apollo/client' import { INVITE_FIELDS } from '../../fragments/invites' import AccordianItem from '../../components/accordian-item' import styles from '../../styles/invites.module.css' +import Invite from '../../components/invite' export const InviteSchema = Yup.object({ gift: Yup.number().typeError('must be a number') @@ -73,54 +74,6 @@ function InviteForm () { ) } -function Invite ({ invite, active }) { - const [revokeInvite] = useMutation( - gql` - ${INVITE_FIELDS} - mutation revokeInvite($id: ID!) { - revokeInvite(id: $id) { - ...InviteFields - } - }` - ) - - return ( -
- -
- {invite.gift} sat gift - \ - {invite.invitees.length} joined{invite.limit ? ` of ${invite.limit}` : ''} - {active - ? ( - <> - \ - revokeInvite({ variables: { id: invite.id } })} - >revoke - - ) - - : invite.revoked && ( - <> - \ - revoked - - )} -
-
- ) -} - function InviteList ({ name, invites }) { return (