diff --git a/api/webPush/index.js b/api/webPush/index.js index a09cb52a..2bb32171 100644 --- a/api/webPush/index.js +++ b/api/webPush/index.js @@ -37,7 +37,12 @@ const createUserFilter = (tag) => { REPLY: 'noteAllDescendants', MENTION: 'noteMentions', TIP: 'noteItemSats', - FORWARDEDTIP: 'noteForwardedSats' + FORWARDEDTIP: 'noteForwardedSats', + REFERRAL: 'noteInvites', + INVITE: 'noteInvites', + EARN: 'noteEarning', + DEPOSIT: 'noteDeposits', + STREAK: 'noteCowboyHat' } const key = tagMap[tag.split('-')[0]] return key ? { user: { [key]: true } } : undefined diff --git a/components/notifications.js b/components/notifications.js index b440a6cb..07e74984 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -11,7 +11,7 @@ import { dayMonthYear, timeSince } from '../lib/time' import Link from 'next/link' import Check from '../svgs/check-double-line.svg' import HandCoin from '../svgs/hand-coin-fill.svg' -import { COMMENT_DEPTH_LIMIT } from '../lib/constants' +import { COMMENT_DEPTH_LIMIT, LOST_BLURBS, FOUND_BLURBS } from '../lib/constants' import CowboyHatIcon from '../svgs/cowboy.svg' import BaldIcon from '../svgs/bald.svg' import { RootProvider } from './root' @@ -122,25 +122,7 @@ const defaultOnClick = n => { function Streak ({ n }) { function blurb (n) { - const index = Number(n.id) % 6 - const FOUND_BLURBS = [ - 'The harsh frontier is no place for the unprepared. This hat will protect you from the sun, dust, and other elements Mother Nature throws your way.', - 'A cowboy is nothing without a cowboy hat. Take good care of it, and it will protect you from the sun, dust, and other elements on your journey.', - "This is not just a hat, it's a matter of survival. Take care of this essential tool, and it will shield you from the scorching sun and the elements.", - "A cowboy hat isn't just a fashion statement. It's your last defense against the unforgiving elements of the Wild West. Hang onto it tight.", - "A good cowboy hat is worth its weight in gold, shielding you from the sun, wind, and dust of the western frontier. Don't lose it.", - 'Your cowboy hat is the key to your survival in the wild west. Treat it with respect and it will protect you from the elements.' - ] - - const LOST_BLURBS = [ - 'your cowboy hat was taken by the wind storm that blew in from the west. No worries, a true cowboy always finds another hat.', - "you left your trusty cowboy hat in the saloon before leaving town. You'll need a replacement for the long journey west.", - 'you lost your cowboy hat in a wild shoot-out on the outskirts of town. Tough luck, tIme to start searching for another one.', - 'you ran out of food and had to trade your hat for supplies. Better start looking for another hat.', - "your hat was stolen by a mischievous prairie dog. You won't catch the dog, but you can always find another hat.", - 'you lost your hat while crossing the river on your journey west. Maybe you can find a replacement hat in the next town.' - ] - + const index = Number(n.id) % Math.min(FOUND_BLURBS.length, LOST_BLURBS.length) if (n.days) { return `After ${numWithUnits(n.days, { abbreviate: false, diff --git a/lib/constants.js b/lib/constants.js index d679689b..cc1eeb7d 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -52,3 +52,20 @@ export const ANON_COMMENT_FEE = 100 export const SSR = typeof window === 'undefined' export const MAX_FORWARDS = 5 export const LNURLP_COMMENT_MAX_LENGTH = 1000 + +export const FOUND_BLURBS = [ + 'The harsh frontier is no place for the unprepared. This hat will protect you from the sun, dust, and other elements Mother Nature throws your way.', + 'A cowboy is nothing without a cowboy hat. Take good care of it, and it will protect you from the sun, dust, and other elements on your journey.', + "This is not just a hat, it's a matter of survival. Take care of this essential tool, and it will shield you from the scorching sun and the elements.", + "A cowboy hat isn't just a fashion statement. It's your last defense against the unforgiving elements of the Wild West. Hang onto it tight.", + "A good cowboy hat is worth its weight in gold, shielding you from the sun, wind, and dust of the western frontier. Don't lose it.", + 'Your cowboy hat is the key to your survival in the wild west. Treat it with respect and it will protect you from the elements.' +] +export const LOST_BLURBS = [ + 'your cowboy hat was taken by the wind storm that blew in from the west. No worries, a true cowboy always finds another hat.', + "you left your trusty cowboy hat in the saloon before leaving town. You'll need a replacement for the long journey west.", + 'you lost your cowboy hat in a wild shoot-out on the outskirts of town. Tough luck, tIme to start searching for another one.', + 'you ran out of food and had to trade your hat for supplies. Better start looking for another hat.', + "your hat was stolen by a mischievous prairie dog. You won't catch the dog, but you can always find another hat.", + 'you lost your hat while crossing the river on your journey west. Maybe you can find a replacement hat in the next town.' +] diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index 2dc0e4ab..8cf95a0f 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -11,6 +11,7 @@ import { decode, getToken } from 'next-auth/jwt' import { NodeNextRequest } from 'next/dist/server/base-http/node' import jose1 from 'jose1' import { schnorr } from '@noble/curves/secp256k1' +import { sendUserNotification } from '../../../api/webPush' function getCallbacks (req) { return { @@ -42,6 +43,7 @@ function getCallbacks (req) { const referrer = await prisma.user.findUnique({ where: { name: req.cookies.sn_referrer } }) if (referrer) { await prisma.user.update({ where: { id: user.id }, data: { referrerId: referrer.id } }) + sendUserNotification(referrer.id, { title: 'someone joined via one of your referral links', tag: 'REFERRAL' }).catch(console.error) } } diff --git a/pages/invites/[id].js b/pages/invites/[id].js index fda45c63..9270b0f8 100644 --- a/pages/invites/[id].js +++ b/pages/invites/[id].js @@ -9,6 +9,7 @@ import getSSRApolloClient from '../../api/ssrApollo' import Link from 'next/link' import { CenterLayout } from '../../components/layout' import { getAuthOptions } from '../api/auth/[...nextauth]' +import { sendUserNotification } from '../../api/webPush' export async function getServerSideProps ({ req, res, query: { id, error = null } }) { const session = await getServerSession(req, res, getAuthOptions(req)) @@ -36,6 +37,8 @@ export async function getServerSideProps ({ req, res, query: { id, error = null // catch any errors and just ignore them for now await serialize(models, models.$queryRawUnsafe('SELECT invite_drain($1::INTEGER, $2::INTEGER)', session.user.id, id)) + const invite = await models.invite.findUnique({ where: { id } }) + sendUserNotification(invite.userId, { title: 'your invite has been redeemed', tag: 'INVITE' }).catch(console.error) } catch (e) { console.log(e) } diff --git a/sw/index.js b/sw/index.js index baf6525f..97268b3c 100644 --- a/sw/index.js +++ b/sw/index.js @@ -6,6 +6,7 @@ import { NetworkOnly } from 'workbox-strategies' import { enable } from 'workbox-navigation-preload' import manifest from './precache-manifest.json' import ServiceWorkerStorage from 'serviceworker-storage' +import { numWithUnits } from '../lib/format' // comment out to enable workbox console logs self.__WB_DISABLE_DEV_LOGS = true @@ -49,7 +50,8 @@ self.addEventListener('push', async function (event) { if (!payload) return const { tag } = payload.options event.waitUntil((async () => { - if (!['REPLY', 'MENTION'].includes(tag)) { + // TIP and EARN notifications simply replace the previous notifications + if (!tag || ['TIP', 'EARN'].includes(tag.split('-')[0])) { return self.registration.showNotification(payload.title, payload.options) } @@ -66,15 +68,26 @@ self.addEventListener('push', async function (event) { } const currentNotification = notifications[0] const amount = currentNotification.data?.amount ? currentNotification.data.amount + 1 : 2 - let title = '' + let newTitle = '' + const data = {} if (tag === 'REPLY') { - title = `You have ${amount} new replies` + newTitle = `You have ${amount} new replies` } else if (tag === 'MENTION') { - title = `You were mentioned ${amount} times` + newTitle = `You were mentioned ${amount} times` + } else if (tag === 'REFERRAL') { + newTitle = `${amount} stackers joined via your referral links` + } else if (tag === 'INVITE') { + newTitle = `your invite has been redeemed by ${amount} stackers` + } else if (tag === 'DEPOSIT') { + const currentSats = currentNotification.data.sats + const incomingSats = payload.options.data.sats + const newSats = currentSats + incomingSats + data.sats = newSats + newTitle = `${numWithUnits(newSats, { abbreviate: false })} were deposited in your account` } currentNotification.close() const { icon } = currentNotification - return self.registration.showNotification(title, { icon, tag, data: { url: '/notifications', amount } }) + return self.registration.showNotification(newTitle, { icon, tag, data: { url: '/notifications', amount, ...data } }) })()) }) diff --git a/worker/earn.js b/worker/earn.js index 58db4344..5b7471cc 100644 --- a/worker/earn.js +++ b/worker/earn.js @@ -1,5 +1,7 @@ import serialize from '../api/resolvers/serial.js' +import { sendUserNotification } from '../api/webPush/index.js' import { ANON_USER_ID } from '../lib/constants.js' +import { msatsToSats, numWithUnits } from '../lib/format.js' const ITEM_EACH_REWARD = 4.0 const UPVOTE_EACH_REWARD = 4.0 @@ -160,6 +162,10 @@ export function earn ({ models }) { await serialize(models, models.$executeRaw`SELECT earn(${earner.userId}::INTEGER, ${earnings}, ${now}::timestamp without time zone, ${earner.type}::"EarnType", ${earner.id}::INTEGER, ${earner.rank}::INTEGER)`) + sendUserNotification(earner.userId, { + title: `you stacked ${numWithUnits(msatsToSats(earnings), { abbreviate: false })} in rewards`, + tag: 'EARN' + }).catch(console.error) } }) diff --git a/worker/streak.js b/worker/streak.js index 46e578c7..4f90b2dd 100644 --- a/worker/streak.js +++ b/worker/streak.js @@ -1,3 +1,6 @@ +import { sendUserNotification } from '../api/webPush' +import { FOUND_BLURBS, LOST_BLURBS } from '../lib/constants' + const STREAK_THRESHOLD = 100 export function computeStreaks ({ models }) { @@ -7,8 +10,8 @@ export function computeStreaks ({ models }) { // get all eligible users in the last day // if the user doesn't have an active streak, add one // if they have an active streak but didn't maintain it, end it - await models.$executeRawUnsafe( - `WITH day_streaks (id) AS ( + const endingStreaks = await models.$queryRaw` + WITH day_streaks (id) AS ( SELECT "userId" FROM ((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent @@ -58,7 +61,20 @@ export function computeStreaks ({ models }) { UPDATE "Streak" SET "endedAt" = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date, updated_at = now_utc() FROM ending_streaks - WHERE ending_streaks.id = "Streak"."userId" AND "endedAt" IS NULL`) + WHERE ending_streaks.id = "Streak"."userId" AND "endedAt" IS NULL + RETURNING "Streak".id, ending_streaks."id" AS "userId"` + + Promise.allSettled( + endingStreaks.map(({ id, userId }) => { + const index = id % LOST_BLURBS.length + const blurb = LOST_BLURBS[index] + return sendUserNotification(userId, { + title: 'you lost your cowboy hat', + body: blurb, + tag: 'STREAK-LOST' + }).catch(console.error) + }) + ) console.log('done computing streaks') } @@ -69,7 +85,7 @@ export function checkStreak ({ models }) { console.log('checking streak', id) // if user is actively streaking skip - const streak = await models.streak.findFirst({ + let streak = await models.streak.findFirst({ where: { userId: Number(id), endedAt: null @@ -81,7 +97,7 @@ export function checkStreak ({ models }) { return } - await models.$executeRaw` + [streak] = await models.$queryRaw` WITH streak_started (id) AS ( SELECT "userId" FROM @@ -103,8 +119,20 @@ export function checkStreak ({ models }) { ) INSERT INTO "Streak" ("userId", "startedAt", created_at, updated_at) SELECT id, (now() AT TIME ZONE 'America/Chicago')::date, now_utc(), now_utc() - FROM streak_started` + FROM streak_started + RETURNING "Streak".id` console.log('done checking streak', id) + + if (!streak) return + + // new streak started for user + const index = streak.id % FOUND_BLURBS.length + const blurb = FOUND_BLURBS[index] + sendUserNotification(id, { + title: 'you found a cowboy hat', + body: blurb, + tag: 'STREAK-FOUND' + }).catch(console.error) } } diff --git a/worker/wallet.js b/worker/wallet.js index 063bbbcc..dd9c14c7 100644 --- a/worker/wallet.js +++ b/worker/wallet.js @@ -1,6 +1,8 @@ import serialize from '../api/resolvers/serial.js' import { getInvoice, getPayment, cancelHodlInvoice } from 'ln-service' import { datePivot } from '../lib/time.js' +import { sendUserNotification } from '../api/webPush/index.js' +import { msatsToSats, numWithUnits } from '../lib/format' const walletOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true } @@ -29,6 +31,12 @@ export function checkInvoice ({ boss, models, lnd }) { // we manually confirm them when we settle them await serialize(models, models.$executeRaw`SELECT confirm_invoice(${inv.id}, ${Number(inv.received_mtokens)})`) + sendUserNotification(dbInv.userId, { + title: `${numWithUnits(msatsToSats(inv.received_mtokens), { abbreviate: false })} were deposited in your account`, + body: dbInv.comment || undefined, + tag: 'DEPOSIT', + data: { sats: msatsToSats(inv.received_mtokens) } + }).catch(console.error) return boss.send('nip57', { hash }) }