diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js
index 26e8c4872..930621f60 100644
--- a/api/resolvers/notifications.js
+++ b/api/resolvers/notifications.js
@@ -5,6 +5,7 @@ import { pushSubscriptionSchema, validateSchema } from '@/lib/validate'
import { replyToSubscription } from '@/lib/webPush'
import { getSub } from './sub'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
+import { msatsToSats } from '@/lib/format'
export default {
Query: {
@@ -361,6 +362,21 @@ export default {
LIMIT ${LIMIT})`
)
+ if (meFull.noteSatSummary) {
+ queries.push(
+ `(SELECT 'stats_' || date_trunc('day', t)::text as id,
+ t AS "sortTime", NULL as "earnedSats", 'SatSummary' AS type
+ FROM user_stats_days
+ WHERE id = $1
+ AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '2 day')
+ AND t <= $2
+ GROUP BY t
+ HAVING sum(msats_stacked) != 0 OR sum(msats_spent) != 0
+ ORDER BY "sortTime" DESC
+ LIMIT 1)`
+ )
+ }
+
const notifications = await models.$queryRawUnsafe(
`SELECT id, "sortTime", "earnedSats", type,
"sortTime" AS "minSortTime"
@@ -486,6 +502,29 @@ export default {
return res.length ? res[0].type : null
}
},
+ SatSummary: {
+ date: async (n, args, { models }) => {
+ return new Date(n.sortTime)
+ },
+ stacked: async (n, args, { me, models }) => {
+ const [{ stacked }] = await models.$queryRaw`
+ SELECT sum(msats_stacked) as stacked
+ FROM user_stats_days
+ WHERE id = ${Number(me.id)}
+ AND t = ${n.sortTime}::timestamp
+ `
+ return (stacked && msatsToSats(stacked)) || 0
+ },
+ spent: async (n, args, { me, models }) => {
+ const [{ spent }] = await models.$queryRaw`
+ SELECT sum(msats_spent) as spent
+ FROM user_stats_days
+ WHERE id = ${Number(me.id)}
+ AND t = ${n.sortTime}::timestamp
+ `
+ return (spent && msatsToSats(spent)) || 0
+ }
+ },
Earn: {
sources: async (n, args, { me, models }) => {
const [sources] = await models.$queryRawUnsafe(`
diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index f131fa929..77225f91e 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -503,6 +503,20 @@ export default {
}
}
+ if (user.noteSatSummary) {
+ const [satSummary] = await models.$queryRawUnsafe(`
+ SELECT EXISTS(
+ SELECT *
+ FROM user_stats_days
+ WHERE "user_stats_days"."id" = $1
+ AND t > $2
+ LIMIT 1)`, me.id, lastChecked)
+ if (satSummary.exists) {
+ foundNotes()
+ return true
+ }
+ }
+
const subStatus = await models.sub.findFirst({
where: {
userId: me.id,
diff --git a/api/typeDefs/notifications.js b/api/typeDefs/notifications.js
index 4eabb3564..c70e207fa 100644
--- a/api/typeDefs/notifications.js
+++ b/api/typeDefs/notifications.js
@@ -82,6 +82,14 @@ export default gql`
type: String!
}
+ type SatSummary {
+ id: ID!
+ sortTime: Date!
+ date: Date!
+ stacked: Int
+ spent: Int
+ }
+
type Earn {
id: ID!
earnedSats: Int!
@@ -155,7 +163,7 @@ export default gql`
union Notification = Reply | Votification | Mention
| Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral
- | Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
+ | Streak | SatSummary | FollowActivity | ForwardedVotification | Revenue | SubStatus
| TerritoryPost | TerritoryTransfer | Reminder | ItemMention | Invoicification
| ReferralReward
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index bfefe7e3d..204a7ed0c 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -94,6 +94,7 @@ export default gql`
nostrPubkey: String
nostrRelays: [String!]
noteAllDescendants: Boolean!
+ noteSatSummary: Boolean!
noteCowboyHat: Boolean!
noteDeposits: Boolean!,
noteWithdrawals: Boolean!,
@@ -171,6 +172,7 @@ export default gql`
nostrPubkey: String
nostrRelays: [String!]
noteAllDescendants: Boolean!
+ noteSatSummary: Boolean!
noteCowboyHat: Boolean!
noteDeposits: Boolean!
noteWithdrawals: Boolean!
diff --git a/components/notifications.js b/components/notifications.js
index d159c74d1..7c1146723 100644
--- a/components/notifications.js
+++ b/components/notifications.js
@@ -13,6 +13,7 @@ import HandCoin from '@/svgs/hand-coin-fill.svg'
import UserAdd from '@/svgs/user-add-fill.svg'
import { LOST_BLURBS, FOUND_BLURBS, UNKNOWN_LINK_REL } from '@/lib/constants'
import CowboyHatIcon from '@/svgs/cowboy.svg'
+import ScalesIcon from '@/svgs/scales-of-justice.svg'
import BaldIcon from '@/svgs/bald.svg'
import GunIcon from '@/svgs/revolver.svg'
import HorseIcon from '@/svgs/horse.svg'
@@ -57,6 +58,7 @@ function Notification ({ n, fresh }) {
(type === 'WithdrawlPaid' && ) ||
(type === 'Referral' && ) ||
(type === 'Streak' && ) ||
+ (type === 'SatSummary' && ) ||
(type === 'Votification' && ) ||
(type === 'ForwardedVotification' && ) ||
(type === 'Mention' && ) ||
@@ -162,6 +164,7 @@ const defaultOnClick = n => {
if (type === 'Referral') return { href: '/referrals/month' }
if (type === 'ReferralReward') return { href: '/referrals/month' }
if (type === 'Streak') return {}
+ if (type === 'SatSummary') return { href: '/satistics?inc=stacked,spent' }
if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` }
if (!n.item) return {}
@@ -200,6 +203,18 @@ function Streak ({ n }) {
)
}
+function SatSummary ({ n }) {
+ return (
+
+
+
+
you stacked {numWithUnits(n.stacked, { abbreviate: false })} and spent {numWithUnits(n.spent, { abbreviate: false })}
+
on {dayMonthYear(new Date(n.date))}
+
+
+ )
+}
+
function EarnNotification ({ n }) {
const time = n.minSortTime === n.sortTime ? dayMonthYear(new Date(n.minSortTime)) : `${dayMonthYear(new Date(n.minSortTime))} to ${dayMonthYear(new Date(n.sortTime))}`
diff --git a/fragments/notifications.js b/fragments/notifications.js
index 0915ccdf1..5cd15577f 100644
--- a/fragments/notifications.js
+++ b/fragments/notifications.js
@@ -88,6 +88,13 @@ export const NOTIFICATIONS = gql`
days
type
}
+ ... on SatSummary {
+ id
+ sortTime
+ date
+ stacked
+ spent
+ }
... on Earn {
id
sortTime
diff --git a/fragments/users.js b/fragments/users.js
index c65cc2a06..9b3424613 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -83,6 +83,7 @@ export const SETTINGS_FIELDS = gql`
noteWithdrawals
noteInvites
noteJobIndicator
+ noteSatSummary
noteCowboyHat
noteForwardedSats
hideInvoiceDesc
diff --git a/lib/apollo.js b/lib/apollo.js
index 6e2eeab1a..f412fbfae 100644
--- a/lib/apollo.js
+++ b/lib/apollo.js
@@ -63,6 +63,7 @@ function getClient (uri) {
'WithdrawlPaid',
'Referral',
'Streak',
+ 'SatSummary',
'FollowActivity',
'ForwardedVotification',
'Revenue',
diff --git a/lib/webPush.js b/lib/webPush.js
index 88b20047d..537925b70 100644
--- a/lib/webPush.js
+++ b/lib/webPush.js
@@ -46,6 +46,7 @@ const createUserFilter = (tag) => {
EARN: 'noteEarning',
DEPOSIT: 'noteDeposits',
WITHDRAWAL: 'noteWithdrawals',
+ DAILY_STATS: 'noteSatSummary',
STREAK: 'noteCowboyHat'
}
const key = tagMap[tag.split('-')[0]]
@@ -392,6 +393,18 @@ export async function notifyStreakLost (userId, streak) {
}
}
+export async function notifySatSummary (userId, stacked, spent) {
+ try {
+ await sendUserNotification(userId, {
+ title: 'your daily sat summary is ready',
+ body: `you stacked ${numWithUnits(stacked, { abbreviate: false })} and spent ${numWithUnits(spent, { abbreviate: false })}`,
+ tag: 'DAILY_STATS'
+ })
+ } catch (err) {
+ console.error(err)
+ }
+}
+
export async function notifyReminder ({ userId, item, itemId }) {
try {
await sendUserNotification(userId, {
diff --git a/pages/settings/index.js b/pages/settings/index.js
index 4f1e912d9..1aa09ed24 100644
--- a/pages/settings/index.js
+++ b/pages/settings/index.js
@@ -138,6 +138,7 @@ export default function Settings ({ ssrData }) {
noteInvites: settings?.noteInvites,
noteJobIndicator: settings?.noteJobIndicator,
noteCowboyHat: settings?.noteCowboyHat,
+ noteSatSummary: settings?.noteSatSummary,
noteForwardedSats: settings?.noteForwardedSats,
hideInvoiceDesc: settings?.hideInvoiceDesc,
autoDropBolt11s: settings?.autoDropBolt11s,
@@ -335,6 +336,11 @@ export default function Settings ({ ssrData }) {
name='noteJobIndicator'
groupClassName='mb-0'
/>
+
\ No newline at end of file
diff --git a/sw/eventListener.js b/sw/eventListener.js
index fab3e910a..af807b518 100644
--- a/sw/eventListener.js
+++ b/sw/eventListener.js
@@ -67,7 +67,7 @@ export function onPush (sw) {
// if there is no tag or the tag is one of the following
// we show the notification immediately
const immediatelyShowNotification = (tag) =>
- !tag || ['TIP', 'FORWARDEDTIP', 'EARN', 'STREAK', 'TERRITORY_TRANSFER'].includes(tag.split('-')[0])
+ !tag || ['TIP', 'FORWARDEDTIP', 'EARN', 'STREAK', 'TERRITORY_TRANSFER', 'DAILY_STATS'].includes(tag.split('-')[0])
// merge notifications with the same tag
const mergeNotification = (event, sw, payload, currentNotifications, tag, nid) => {
diff --git a/worker/satSummary.js b/worker/satSummary.js
new file mode 100644
index 000000000..2a036fecb
--- /dev/null
+++ b/worker/satSummary.js
@@ -0,0 +1,33 @@
+import { notifySatSummary } from '@/lib/webPush'
+import { msatsToSats } from '@/lib/format'
+export async function dailySatSummary ({ models }) {
+ console.log('running dailySatSummary')
+ try {
+ const stats = await models.$queryRaw`
+ SELECT
+ id as userId, sum(msats_stacked) as stacked, sum(msats_spent) as spent
+ FROM user_stats_days
+ WHERE t >= date_trunc('day', CURRENT_DATE - INTERVAL '1 day')
+ AND t <= date_trunc('day', CURRENT_DATE)
+ GROUP BY id
+ HAVING sum(msats_stacked) != 0 OR sum(msats_spent) != 0
+ `
+
+ if (stats.length) {
+ for (const stat of stats) {
+ const user = await models.user.findUnique({ where: { id: stat.userid } })
+ if (user && user.noteSatSummary) {
+ await notifySatSummary(
+ stat.userid,
+ (stats.stacked && msatsToSats(stats.stacked)) || 0,
+ (stats.spent && msatsToSats(stats.spent)) || 0
+ )
+ }
+ }
+ }
+ } catch (err) {
+ console.error('failed to process daily sat summary', err)
+ } finally {
+ console.log('finished dailySatSummary')
+ }
+}
diff --git a/worker/views.js b/worker/views.js
index e358b5f62..92fbc59c8 100644
--- a/worker/views.js
+++ b/worker/views.js
@@ -1,4 +1,5 @@
import createPrisma from '@/lib/create-prisma'
+import { dailySatSummary } from './satSummary'
const viewPrefixes = ['reg_growth', 'spender_growth', 'item_growth', 'spending_growth',
'stackers_growth', 'stacking_growth', 'user_stats', 'sub_stats']
@@ -22,6 +23,7 @@ export async function views ({ data: { period } = { period: 'days' } }) {
await models.$queryRawUnsafe(`REFRESH MATERIALIZED VIEW CONCURRENTLY ${view}_${period}`)
}
} finally {
+ if (period === 'days') await dailySatSummary({ models }).catch(console.error) // run dailySatSummary after views-days refresh
await models.$disconnect()
}
}