Skip to content

Commit 5145c7f

Browse files
committed
territoryRevenue; territoryStatus; wip referralReward
1 parent 01b021a commit 5145c7f

File tree

10 files changed

+166
-33
lines changed

10 files changed

+166
-33
lines changed

api/lnd/index.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { cachedFetcher } from '@/lib/fetch'
22
import { toPositiveNumber } from '@/lib/format'
33
import { authenticatedLndGrpc } from '@/lib/lnd'
4-
import { getIdentity, getHeight, getWalletInfo, getNode, getPayment } from 'ln-service'
4+
import { getIdentity, getHeight, getWalletInfo, getNode, getPayment, parsePaymentRequest } from 'ln-service'
55
import { datePivot } from '@/lib/time'
66
import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
77

@@ -23,11 +23,34 @@ getWalletInfo({ lnd }, (err, result) => {
2323
})
2424

2525
export async function estimateRouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) {
26+
// if the payment request includes us as route hint, we needd to use the destination and amount
27+
// otherwise, this will fail with a self-payment error
28+
if (request) {
29+
const inv = parsePaymentRequest({ request })
30+
const ourPubkey = await getOurPubkey({ lnd })
31+
if (Array.isArray(inv.routes)) {
32+
for (const route of inv.routes) {
33+
if (Array.isArray(route)) {
34+
for (const hop of route) {
35+
if (hop.public_key === ourPubkey) {
36+
console.log('estimateRouteFee ignoring self-payment route')
37+
request = false
38+
break
39+
}
40+
}
41+
}
42+
}
43+
}
44+
}
45+
2646
return await new Promise((resolve, reject) => {
2747
const params = {}
48+
2849
if (request) {
50+
console.log('estimateRouteFee using payment request')
2951
params.payment_request = request
3052
} else {
53+
console.log('estimateRouteFee using destination and amount')
3154
params.dest = Buffer.from(destination, 'hex')
3255
params.amt_sat = tokens ? toPositiveNumber(tokens) : toPositiveNumber(BigInt(mtokens) / BigInt(1e3))
3356
}

components/comment.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import itemStyles from './item.module.css'
22
import styles from './comment.module.css'
33
import Text, { SearchText } from './text'
44
import Link from 'next/link'
5-
import Reply, { ReplyOnAnotherPage } from './reply'
5+
import Reply from './reply'
66
import { useEffect, useMemo, useRef, useState } from 'react'
77
import UpVote from './upvote'
88
import Eye from '@/svgs/eye-fill.svg'
@@ -27,6 +27,7 @@ import Pin from '@/svgs/pushpin-fill.svg'
2727
import LinkToContext from './link-to-context'
2828
import Boost from './boost-button'
2929
import { gql, useApolloClient } from '@apollo/client'
30+
import classNames from 'classnames'
3031

3132
function Parent ({ item, rootText }) {
3233
const root = useRoot()
@@ -81,6 +82,7 @@ export function CommentFlat ({ item, rank, siblingComments, ...props }) {
8182
<LinkToContext
8283
className='py-2'
8384
onClick={e => {
85+
e.preventDefault()
8486
router.push(href, as)
8587
}}
8688
href={href}
@@ -141,7 +143,7 @@ export default function Comment ({
141143
}
142144
}, [item.id])
143145

144-
const bottomedOut = depth === COMMENT_DEPTH_LIMIT
146+
const bottomedOut = depth === COMMENT_DEPTH_LIMIT || (item.comments?.comments.length === 0 && item.nDirectComments > 0)
145147
// Don't show OP badge when anon user comments on anon user posts
146148
const op = root.user.name === item.user.name && Number(item.user.id) !== USER_ID.anon
147149
? 'OP'
@@ -243,7 +245,7 @@ export default function Comment ({
243245
</div>
244246
{collapse !== 'yep' && (
245247
bottomedOut
246-
? <div className={styles.children}><ReplyOnAnotherPage item={item} /></div>
248+
? <div className={styles.children}><div className={classNames(styles.comment, 'mt-3')}><ReplyOnAnotherPage item={item} /></div></div>
247249
: (
248250
<div className={styles.children}>
249251
{item.outlawed && !me?.privates?.wildWestMode
@@ -254,13 +256,13 @@ export default function Comment ({
254256
</Reply>}
255257
{children}
256258
<div className={styles.comments}>
257-
{item.comments.comments && !noComments
259+
{!noComments && item.comments?.comments
258260
? (
259261
<>
260262
{item.comments.comments.map((item) => (
261263
<Comment depth={depth + 1} key={item.id} item={item} />
262264
))}
263-
{item.comments.comments.length < item.nDirectComments && <ViewAllReplies id={item.id} nshown={item.comments.comments.length} nhas={item.nDirectComments} />}
265+
{item.comments.comments.length < item.nDirectComments && <ViewAllReplies id={item.id} nhas={item.ncomments} />}
264266
</>
265267
)
266268
: null}
@@ -285,6 +287,22 @@ export function ViewAllReplies ({ id, nshown, nhas }) {
285287
)
286288
}
287289

290+
function ReplyOnAnotherPage ({ item }) {
291+
const root = useRoot()
292+
const rootId = commentSubTreeRootId(item, root)
293+
294+
let text = 'reply on another page'
295+
if (item.ncomments > 0) {
296+
text = `view all ${item.ncomments} replies`
297+
}
298+
299+
return (
300+
<Link href={`/items/${rootId}?commentId=${item.id}`} as={`/items/${rootId}`} className='d-block pb-2 fw-bold text-muted'>
301+
{text}
302+
</Link>
303+
)
304+
}
305+
288306
export function CommentSkeleton ({ skeletonChildren }) {
289307
return (
290308
<div className={styles.comment}>

components/reply.js

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,17 @@ import styles from './reply.module.css'
33
import { COMMENTS } from '@/fragments/comments'
44
import { useMe } from './me'
55
import { forwardRef, useCallback, useEffect, useState, useRef, useMemo } from 'react'
6-
import Link from 'next/link'
76
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
87
import { commentsViewedAfterComment } from '@/lib/new-comments'
98
import { commentSchema } from '@/lib/validate'
109
import { ItemButtonBar } from './post'
1110
import { useShowModal } from './modal'
1211
import { Button } from 'react-bootstrap'
1312
import { useRoot } from './root'
14-
import { commentSubTreeRootId } from '@/lib/item'
1513
import { CREATE_COMMENT } from '@/fragments/paidAction'
1614
import useItemSubmit from './use-item-submit'
1715
import gql from 'graphql-tag'
1816

19-
export function ReplyOnAnotherPage ({ item }) {
20-
const rootId = commentSubTreeRootId(item)
21-
22-
let text = 'reply on another page'
23-
if (item.ncomments > 0) {
24-
text = 'view replies'
25-
}
26-
27-
return (
28-
<Link href={`/items/${rootId}?commentId=${item.id}`} as={`/items/${rootId}`} className='d-block py-3 fw-bold text-muted'>
29-
{text}
30-
</Link>
31-
)
32-
}
33-
3417
export default forwardRef(function Reply ({
3518
item,
3619
replyOpen,

lib/item.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ export const deleteReminders = async ({ id, userId, models }) => {
105105
})
106106
}
107107

108-
export const commentSubTreeRootId = (item) => {
109-
if (item.root?.ncomments > FULL_COMMENTS_THRESHOLD) {
108+
export const commentSubTreeRootId = (item, root) => {
109+
if (item.root?.ncomments > FULL_COMMENTS_THRESHOLD || root?.ncomments > FULL_COMMENTS_THRESHOLD) {
110110
return item.id
111111
}
112112

lib/webPush.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import webPush from 'web-push'
22
import removeMd from 'remove-markdown'
33
import { COMMENT_DEPTH_LIMIT, FOUND_BLURBS, LOST_BLURBS } from './constants'
44
import { msatsToSats, numWithUnits } from './format'
5+
import { nextBillingWithGrace } from '@/lib/territory'
56
import models from '@/api/models'
67
import { isMuted } from '@/lib/user'
78
import { Prisma } from '@prisma/client'
@@ -103,6 +104,7 @@ async function sendUserNotification (userId, notification) {
103104
const subscriptions = await models.pushSubscription.findMany({
104105
where: { userId, ...userFilter }
105106
})
107+
console.log('notification', payload)
106108
await Promise.allSettled(
107109
subscriptions.map(subscription => sendNotification(subscription, payload))
108110
)
@@ -373,16 +375,49 @@ export const notifyTerritoryTransfer = async ({ models, sub, to }) => {
373375
}
374376
}
375377

378+
export const notifyTerritoryStatusChange = async ({ sub }) => {
379+
const dueDate = nextBillingWithGrace(sub)
380+
const days = Math.ceil((new Date(dueDate) - new Date()) / (1000 * 60 * 60 * 24))
381+
const timeLeft = days === 1 ? 'tomorrow' : `in ${days} days`
382+
const title = sub.status === 'ACTIVE'
383+
? 'your territory is active again'
384+
: sub.status === 'GRACE'
385+
? `your territory payment for ~${sub.name} is due or your territory will be archived ${timeLeft}`
386+
: `~${sub.name} has been archived!`
387+
388+
try {
389+
await sendUserNotification(sub.userId, { title, tag: `TERRITORY_STATUS_CHANGE-${sub.name}` })
390+
} catch (err) {
391+
console.error(err)
392+
}
393+
}
394+
395+
export const notifyTerritoryRevenue = async (subAct) => {
396+
const fmt = msats => numWithUnits(msatsToSats(msats, { abbreviate: false }))
397+
const title = `you earned ${fmt(subAct.msats)} in revenue from ~${subAct.subName}`
398+
try {
399+
await sendUserNotification(subAct.userId, { title, tag: `TERRITORY_REVENUE-${subAct.subName}` })
400+
} catch (err) {
401+
console.error(err)
402+
}
403+
}
404+
405+
// TODO: needs testing, fix rewards not working first
376406
export async function notifyEarner (userId, earnings) {
377407
const fmt = msats => numWithUnits(msatsToSats(msats, { abbreviate: false }))
378408

409+
// TODO: remove
410+
console.log('notifying earners', JSON.stringify(earnings, null, 2))
411+
379412
const title = `you stacked ${fmt(earnings.msats)} in rewards`
380413
const tag = 'EARN'
381414
let body = ''
382415
if (earnings.POST) body += `#${earnings.POST.bestRank} among posts with ${fmt(earnings.POST.msats)} in total\n`
383416
if (earnings.COMMENT) body += `#${earnings.COMMENT.bestRank} among comments with ${fmt(earnings.COMMENT.msats)} in total\n`
384417
if (earnings.TIP_POST) body += `#${earnings.TIP_POST.bestRank} in post zapping with ${fmt(earnings.TIP_POST.msats)} in total\n`
385-
if (earnings.TIP_COMMENT) body += `#${earnings.TIP_COMMENT.bestRank} in comment zapping with ${fmt(earnings.TIP_COMMENT.msats)} in total`
418+
if (earnings.TIP_COMMENT) body += `#${earnings.TIP_COMMENT.bestRank} in comment zapping with ${fmt(earnings.TIP_COMMENT.msats)} in total\n`
419+
if (earnings.FOREVER_REFERRAL) body += `#${earnings.FOREVER_REFERRAL.bestRank} in referral rewards with ${fmt(earnings.FOREVER_REFERRAL.msats)} in total\n`
420+
if (earnings.ONE_DAY_REFERRAL) body += `#${earnings.ONE_DAY_REFERRAL.bestRank} in referral rewards with ${fmt(earnings.ONE_DAY_REFERRAL.msats)} in total`
386421

387422
try {
388423
await sendUserNotification(userId, { title, tag, body })

prisma/migrations/20250118010433_comment_pages/migration.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ BEGIN
8585
EXECUTE ''
8686
|| 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
8787
|| 'FROM ( '
88-
|| ' SELECT "Item".*, item_comments_zaprank_with_me("Item".id, $2, $3, $4, $5, $6, $7 - 1, $8, $9) AS comments '
88+
|| ' SELECT "Item".*, item_comments_zaprank_with_me_limited("Item".id, $2, $3, $4, $5, $6, $7 - 1, $8, $9) AS comments '
8989
|| ' FROM t_item "Item" '
9090
|| ' WHERE "Item"."parentId" = $1 '
9191
|| _order_by
@@ -136,7 +136,7 @@ BEGIN
136136
EXECUTE ''
137137
|| 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
138138
|| 'FROM ( '
139-
|| ' SELECT "Item".*, item_comments("Item".id, $2, $3, $4, $5 - 1, $6, $7) AS comments '
139+
|| ' SELECT "Item".*, item_comments_limited("Item".id, $2, $3, $4, $5 - 1, $6, $7) AS comments '
140140
|| ' FROM t_item "Item" '
141141
|| ' WHERE "Item"."parentId" = $1 '
142142
|| _order_by
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
-- add limit and offset
2+
CREATE OR REPLACE FUNCTION item_comments_limited(
3+
_item_id int, _limit int, _offset int, _grandchild_limit int,
4+
_level int, _where text, _order_by text)
5+
RETURNS jsonb
6+
LANGUAGE plpgsql VOLATILE PARALLEL SAFE AS
7+
$$
8+
DECLARE
9+
result jsonb;
10+
BEGIN
11+
IF _level < 1 THEN
12+
RETURN '[]'::jsonb;
13+
END IF;
14+
15+
EXECUTE 'CREATE TEMP TABLE IF NOT EXISTS t_item ON COMMIT DROP AS '
16+
|| 'WITH RECURSIVE base AS ( '
17+
|| ' (SELECT "Item".*, 1 as level, ROW_NUMBER() OVER () as rn '
18+
|| ' FROM "Item" '
19+
|| ' WHERE "Item"."parentId" = $1 '
20+
|| _order_by || ' '
21+
|| ' LIMIT $2 '
22+
|| ' OFFSET $3) '
23+
|| ' UNION ALL '
24+
|| ' (SELECT "Item".*, b.level + 1, ROW_NUMBER() OVER (PARTITION BY "Item"."parentId" ' || _order_by || ') '
25+
|| ' FROM "Item" '
26+
|| ' JOIN base b ON "Item"."parentId" = b.id '
27+
|| ' WHERE b.level < $5 AND (b.level = 1 OR b.rn <= $4)) '
28+
|| ') '
29+
|| 'SELECT "Item".*, "Item".created_at at time zone ''UTC'' AS "createdAt", "Item".updated_at at time zone ''UTC'' AS "updatedAt", '
30+
|| ' "Item"."invoicePaidAt" at time zone ''UTC'' AS "invoicePaidAtUTC", '
31+
|| ' to_jsonb(users.*) as user '
32+
|| 'FROM base "Item" '
33+
|| 'JOIN users ON users.id = "Item"."userId" '
34+
|| 'WHERE ("Item".level = 1 OR "Item".rn <= $4 - "Item".level + 2) ' || _where
35+
USING _item_id, _limit, _offset, _grandchild_limit, _level, _where, _order_by;
36+
37+
38+
EXECUTE ''
39+
|| 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
40+
|| 'FROM ( '
41+
|| ' SELECT "Item".*, item_comments_limited("Item".id, $2, $3, $4, $5 - 1, $6, $7) AS comments '
42+
|| ' FROM t_item "Item" '
43+
|| ' WHERE "Item"."parentId" = $1 '
44+
|| _order_by
45+
|| ' ) sub'
46+
INTO result USING _item_id, _limit, _offset, _grandchild_limit, _level, _where, _order_by;
47+
RETURN result;
48+
END
49+
$$;

worker/search.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ export async function indexItem ({ data: { id, updatedAt }, apollo, models }) {
117117
}`
118118
})
119119

120+
if (!item) {
121+
console.log('item not found', id)
122+
return
123+
}
124+
120125
// 2. index it with external version based on updatedAt
121126
await _indexItem(item, { models, updatedAt })
122127
}

worker/territory.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import performPaidAction from '@/api/paidAction'
33
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
44
import { nextBillingWithGrace } from '@/lib/territory'
55
import { datePivot } from '@/lib/time'
6+
import { notifyTerritoryStatusChange, notifyTerritoryRevenue } from '@/lib/webPush'
67

78
export async function territoryBilling ({ data: { subName }, boss, models }) {
8-
const sub = await models.sub.findUnique({
9+
let sub = await models.sub.findUnique({
910
where: {
1011
name: subName
1112
}
1213
})
1314

1415
async function territoryStatusUpdate () {
1516
if (sub.status !== 'STOPPED') {
16-
await models.sub.update({
17+
sub = await models.sub.update({
1718
include: { user: true },
1819
where: {
1920
name: subName
@@ -24,7 +25,8 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
2425
}
2526
})
2627
}
27-
28+
// send push notification with the new status
29+
await notifyTerritoryStatusChange({ sub })
2830
// retry billing in one day
2931
await boss.send('territoryBilling', { subName }, { startAfter: datePivot(new Date(), { days: 1 }) })
3032
}
@@ -44,6 +46,9 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
4446
})
4547
if (!result) {
4648
throw new Error('not enough fee credits to auto-renew territory')
49+
} else if (sub.status === 'GRACE' && result.status === 'ACTIVE') {
50+
// if the sub was in grace and we successfully auto-renewed it, send a push notification
51+
await notifyTerritoryStatusChange({ sub })
4752
}
4853
} catch (e) {
4954
console.error(e)
@@ -90,4 +95,17 @@ export async function territoryRevenue ({ models }) {
9095
"stackedMsats" = users."stackedMsats" + "SubActResultTotal".total_msats
9196
FROM "SubActResultTotal"
9297
WHERE users.id = "SubActResultTotal"."userId"`
98+
99+
const territoryRevenue = await models.subAct.findMany({
100+
where: {
101+
createdAt: { // retrieve revenue calculated in the last hour
102+
gte: datePivot(new Date(), { hours: -1 })
103+
},
104+
type: 'REVENUE'
105+
}
106+
})
107+
108+
await Promise.allSettled(
109+
territoryRevenue.map(subAct => notifyTerritoryRevenue(subAct))
110+
)
93111
}

worker/weeklyPosts.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd }
3131
bounty
3232
bountyPaidTo
3333
comments(sort: "top") {
34-
id
34+
comments {
35+
id
36+
}
3537
}
3638
}
3739
}`,
@@ -44,7 +46,7 @@ export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd }
4446
throw new Error('Bounty already paid')
4547
}
4648

47-
const winner = item.comments[0]
49+
const winner = item.comments.comments[0]
4850

4951
if (!winner) {
5052
throw new Error('No winner')

0 commit comments

Comments
 (0)