More push notification types (#530)
* Add push notifications for referrals * Add push notifications for daily rewards * Add push notifications for deposits * Add push notifications for earning cowboy hats * Use streak id to synchronize blurb * Fix usage of magic number for blurbs * Fix missing catch * Add push notification for losing cowboy hats * Fix null in deposit push notification * Add push notification for invites * Don't replace streak push notifications * Fix missing unit in daily reward push notification title * Attach sats to payload options instead of parsing title --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
parent
b3e8280d76
commit
425220d8cb
|
@ -37,7 +37,12 @@ const createUserFilter = (tag) => {
|
||||||
REPLY: 'noteAllDescendants',
|
REPLY: 'noteAllDescendants',
|
||||||
MENTION: 'noteMentions',
|
MENTION: 'noteMentions',
|
||||||
TIP: 'noteItemSats',
|
TIP: 'noteItemSats',
|
||||||
FORWARDEDTIP: 'noteForwardedSats'
|
FORWARDEDTIP: 'noteForwardedSats',
|
||||||
|
REFERRAL: 'noteInvites',
|
||||||
|
INVITE: 'noteInvites',
|
||||||
|
EARN: 'noteEarning',
|
||||||
|
DEPOSIT: 'noteDeposits',
|
||||||
|
STREAK: 'noteCowboyHat'
|
||||||
}
|
}
|
||||||
const key = tagMap[tag.split('-')[0]]
|
const key = tagMap[tag.split('-')[0]]
|
||||||
return key ? { user: { [key]: true } } : undefined
|
return key ? { user: { [key]: true } } : undefined
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { dayMonthYear, timeSince } from '../lib/time'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Check from '../svgs/check-double-line.svg'
|
import Check from '../svgs/check-double-line.svg'
|
||||||
import HandCoin from '../svgs/hand-coin-fill.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 CowboyHatIcon from '../svgs/cowboy.svg'
|
||||||
import BaldIcon from '../svgs/bald.svg'
|
import BaldIcon from '../svgs/bald.svg'
|
||||||
import { RootProvider } from './root'
|
import { RootProvider } from './root'
|
||||||
|
@ -122,25 +122,7 @@ const defaultOnClick = n => {
|
||||||
|
|
||||||
function Streak ({ n }) {
|
function Streak ({ n }) {
|
||||||
function blurb (n) {
|
function blurb (n) {
|
||||||
const index = Number(n.id) % 6
|
const index = Number(n.id) % Math.min(FOUND_BLURBS.length, LOST_BLURBS.length)
|
||||||
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.'
|
|
||||||
]
|
|
||||||
|
|
||||||
if (n.days) {
|
if (n.days) {
|
||||||
return `After ${numWithUnits(n.days, {
|
return `After ${numWithUnits(n.days, {
|
||||||
abbreviate: false,
|
abbreviate: false,
|
||||||
|
|
|
@ -52,3 +52,20 @@ export const ANON_COMMENT_FEE = 100
|
||||||
export const SSR = typeof window === 'undefined'
|
export const SSR = typeof window === 'undefined'
|
||||||
export const MAX_FORWARDS = 5
|
export const MAX_FORWARDS = 5
|
||||||
export const LNURLP_COMMENT_MAX_LENGTH = 1000
|
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.'
|
||||||
|
]
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { decode, getToken } from 'next-auth/jwt'
|
||||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
||||||
import jose1 from 'jose1'
|
import jose1 from 'jose1'
|
||||||
import { schnorr } from '@noble/curves/secp256k1'
|
import { schnorr } from '@noble/curves/secp256k1'
|
||||||
|
import { sendUserNotification } from '../../../api/webPush'
|
||||||
|
|
||||||
function getCallbacks (req) {
|
function getCallbacks (req) {
|
||||||
return {
|
return {
|
||||||
|
@ -42,6 +43,7 @@ function getCallbacks (req) {
|
||||||
const referrer = await prisma.user.findUnique({ where: { name: req.cookies.sn_referrer } })
|
const referrer = await prisma.user.findUnique({ where: { name: req.cookies.sn_referrer } })
|
||||||
if (referrer) {
|
if (referrer) {
|
||||||
await prisma.user.update({ where: { id: user.id }, data: { referrerId: referrer.id } })
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import getSSRApolloClient from '../../api/ssrApollo'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { CenterLayout } from '../../components/layout'
|
import { CenterLayout } from '../../components/layout'
|
||||||
import { getAuthOptions } from '../api/auth/[...nextauth]'
|
import { getAuthOptions } from '../api/auth/[...nextauth]'
|
||||||
|
import { sendUserNotification } from '../../api/webPush'
|
||||||
|
|
||||||
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
|
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
|
||||||
const session = await getServerSession(req, res, getAuthOptions(req))
|
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
|
// catch any errors and just ignore them for now
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
models.$queryRawUnsafe('SELECT invite_drain($1::INTEGER, $2::INTEGER)', session.user.id, id))
|
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) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
|
|
23
sw/index.js
23
sw/index.js
|
@ -6,6 +6,7 @@ import { NetworkOnly } from 'workbox-strategies'
|
||||||
import { enable } from 'workbox-navigation-preload'
|
import { enable } from 'workbox-navigation-preload'
|
||||||
import manifest from './precache-manifest.json'
|
import manifest from './precache-manifest.json'
|
||||||
import ServiceWorkerStorage from 'serviceworker-storage'
|
import ServiceWorkerStorage from 'serviceworker-storage'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
// comment out to enable workbox console logs
|
// comment out to enable workbox console logs
|
||||||
self.__WB_DISABLE_DEV_LOGS = true
|
self.__WB_DISABLE_DEV_LOGS = true
|
||||||
|
@ -49,7 +50,8 @@ self.addEventListener('push', async function (event) {
|
||||||
if (!payload) return
|
if (!payload) return
|
||||||
const { tag } = payload.options
|
const { tag } = payload.options
|
||||||
event.waitUntil((async () => {
|
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)
|
return self.registration.showNotification(payload.title, payload.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,15 +68,26 @@ self.addEventListener('push', async function (event) {
|
||||||
}
|
}
|
||||||
const currentNotification = notifications[0]
|
const currentNotification = notifications[0]
|
||||||
const amount = currentNotification.data?.amount ? currentNotification.data.amount + 1 : 2
|
const amount = currentNotification.data?.amount ? currentNotification.data.amount + 1 : 2
|
||||||
let title = ''
|
let newTitle = ''
|
||||||
|
const data = {}
|
||||||
if (tag === 'REPLY') {
|
if (tag === 'REPLY') {
|
||||||
title = `You have ${amount} new replies`
|
newTitle = `You have ${amount} new replies`
|
||||||
} else if (tag === 'MENTION') {
|
} 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()
|
currentNotification.close()
|
||||||
const { icon } = currentNotification
|
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 } })
|
||||||
})())
|
})())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import serialize from '../api/resolvers/serial.js'
|
import serialize from '../api/resolvers/serial.js'
|
||||||
|
import { sendUserNotification } from '../api/webPush/index.js'
|
||||||
import { ANON_USER_ID } from '../lib/constants.js'
|
import { ANON_USER_ID } from '../lib/constants.js'
|
||||||
|
import { msatsToSats, numWithUnits } from '../lib/format.js'
|
||||||
|
|
||||||
const ITEM_EACH_REWARD = 4.0
|
const ITEM_EACH_REWARD = 4.0
|
||||||
const UPVOTE_EACH_REWARD = 4.0
|
const UPVOTE_EACH_REWARD = 4.0
|
||||||
|
@ -160,6 +162,10 @@ export function earn ({ models }) {
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
models.$executeRaw`SELECT earn(${earner.userId}::INTEGER, ${earnings},
|
models.$executeRaw`SELECT earn(${earner.userId}::INTEGER, ${earnings},
|
||||||
${now}::timestamp without time zone, ${earner.type}::"EarnType", ${earner.id}::INTEGER, ${earner.rank}::INTEGER)`)
|
${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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { sendUserNotification } from '../api/webPush'
|
||||||
|
import { FOUND_BLURBS, LOST_BLURBS } from '../lib/constants'
|
||||||
|
|
||||||
const STREAK_THRESHOLD = 100
|
const STREAK_THRESHOLD = 100
|
||||||
|
|
||||||
export function computeStreaks ({ models }) {
|
export function computeStreaks ({ models }) {
|
||||||
|
@ -7,8 +10,8 @@ export function computeStreaks ({ models }) {
|
||||||
// get all eligible users in the last day
|
// get all eligible users in the last day
|
||||||
// if the user doesn't have an active streak, add one
|
// if the user doesn't have an active streak, add one
|
||||||
// if they have an active streak but didn't maintain it, end it
|
// if they have an active streak but didn't maintain it, end it
|
||||||
await models.$executeRawUnsafe(
|
const endingStreaks = await models.$queryRaw`
|
||||||
`WITH day_streaks (id) AS (
|
WITH day_streaks (id) AS (
|
||||||
SELECT "userId"
|
SELECT "userId"
|
||||||
FROM
|
FROM
|
||||||
((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent
|
((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent
|
||||||
|
@ -58,7 +61,20 @@ export function computeStreaks ({ models }) {
|
||||||
UPDATE "Streak"
|
UPDATE "Streak"
|
||||||
SET "endedAt" = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date, updated_at = now_utc()
|
SET "endedAt" = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date, updated_at = now_utc()
|
||||||
FROM ending_streaks
|
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')
|
console.log('done computing streaks')
|
||||||
}
|
}
|
||||||
|
@ -69,7 +85,7 @@ export function checkStreak ({ models }) {
|
||||||
console.log('checking streak', id)
|
console.log('checking streak', id)
|
||||||
|
|
||||||
// if user is actively streaking skip
|
// if user is actively streaking skip
|
||||||
const streak = await models.streak.findFirst({
|
let streak = await models.streak.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: Number(id),
|
userId: Number(id),
|
||||||
endedAt: null
|
endedAt: null
|
||||||
|
@ -81,7 +97,7 @@ export function checkStreak ({ models }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await models.$executeRaw`
|
[streak] = await models.$queryRaw`
|
||||||
WITH streak_started (id) AS (
|
WITH streak_started (id) AS (
|
||||||
SELECT "userId"
|
SELECT "userId"
|
||||||
FROM
|
FROM
|
||||||
|
@ -103,8 +119,20 @@ export function checkStreak ({ models }) {
|
||||||
)
|
)
|
||||||
INSERT INTO "Streak" ("userId", "startedAt", created_at, updated_at)
|
INSERT INTO "Streak" ("userId", "startedAt", created_at, updated_at)
|
||||||
SELECT id, (now() AT TIME ZONE 'America/Chicago')::date, now_utc(), now_utc()
|
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)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import serialize from '../api/resolvers/serial.js'
|
import serialize from '../api/resolvers/serial.js'
|
||||||
import { getInvoice, getPayment, cancelHodlInvoice } from 'ln-service'
|
import { getInvoice, getPayment, cancelHodlInvoice } from 'ln-service'
|
||||||
import { datePivot } from '../lib/time.js'
|
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 }
|
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
|
// we manually confirm them when we settle them
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
models.$executeRaw`SELECT confirm_invoice(${inv.id}, ${Number(inv.received_mtokens)})`)
|
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 })
|
return boss.send('nip57', { hash })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue