diff --git a/README.md b/README.md index 6e29556e..079e3ca1 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ COMMANDS #### Running specific services -By default all services will be run. If you want to exclude specific services from running, set `COMPOSE_PROFILES` to use one or more of `minimal|images|search|payments|email|capture`. To only run mininal services without images, search, or payments: +By default all services will be run. If you want to exclude specific services from running, set `COMPOSE_PROFILES` to use one or more of `minimal|images|search|payments|wallets|email|capture`. To only run mininal services without images, search, email, wallets, or payments: ```sh $ COMPOSE_PROFILES=minimal ./sndev start diff --git a/api/paidAction/zap.js b/api/paidAction/zap.js index f5c9c169..40dce138 100644 --- a/api/paidAction/zap.js +++ b/api/paidAction/zap.js @@ -11,7 +11,7 @@ export async function getCost ({ sats }) { } export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, tx }) { - const feeMsats = cost / BigInt(100) + const feeMsats = cost / BigInt(10) // 10% fee const zapMsats = cost - feeMsats itemId = parseInt(itemId) @@ -79,12 +79,16 @@ export async function onPaid ({ invoice, actIds }, { models, tx }) { FROM forwardees ), forward AS ( UPDATE users - SET msats = users.msats + forwardees.msats + SET + msats = users.msats + forwardees.msats, + "stackedMsats" = users."stackedMsats" + forwardees.msats FROM forwardees WHERE users.id = forwardees."userId" ) UPDATE users - SET msats = msats + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT + SET + msats = msats + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT, + "stackedMsats" = "stackedMsats" + ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT WHERE id = ${itemAct.item.userId}::INTEGER` // perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt diff --git a/api/resolvers/growth.js b/api/resolvers/growth.js index 9b61837a..70681c92 100644 --- a/api/resolvers/growth.js +++ b/api/resolvers/growth.js @@ -1,26 +1,5 @@ import { timeUnitForRange, whenRange } from '@/lib/time' -export function withClause (range) { - const unit = timeUnitForRange(range) - - return ` - WITH range_values AS ( - SELECT date_trunc('${unit}', $1) as minval, - date_trunc('${unit}', $2) as maxval - ), - times AS ( - SELECT generate_series(minval, maxval, interval '1 ${unit}') as time - FROM range_values - ) - ` -} - -export function intervalClause (range, table) { - const unit = timeUnitForRange(range) - - return `date_trunc('${unit}', "${table}".created_at) >= date_trunc('${unit}', $1) AND date_trunc('${unit}', "${table}".created_at) <= date_trunc('${unit}', $2) ` -} - export function viewIntervalClause (range, view) { const unit = timeUnitForRange(range) return `"${view}".t >= date_trunc('${unit}', timezone('America/Chicago', $1)) AND date_trunc('${unit}', "${view}".t) <= date_trunc('${unit}', timezone('America/Chicago', $2)) ` @@ -42,8 +21,8 @@ export function viewGroup (range, view) { ${view}( date_trunc('hour', timezone('America/Chicago', now())), date_trunc('hour', timezone('America/Chicago', now())), '1 hour'::INTERVAL, 'hour') - WHERE "${view}".t >= date_trunc('${unit}', timezone('America/Chicago', $1)) - AND "${view}".t <= date_trunc('${unit}', timezone('America/Chicago', $2))) + WHERE "${view}".t >= date_trunc('hour', timezone('America/Chicago', $1)) + AND "${view}".t <= date_trunc('hour', timezone('America/Chicago', $2))) ) u` } diff --git a/api/resolvers/item.js b/api/resolvers/item.js index dab28ec4..131d3c50 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -1155,7 +1155,21 @@ export default { if (item.root) { return item.root } - return await getItem(item, { id: item.rootId }, { me, models }) + + // we can't use getItem because activeOrMine will prevent root from being fetched + const [root] = await itemQueryWithMeta({ + me, + models, + query: ` + ${SELECT} + FROM "Item" + ${whereClause( + '"Item".id = $1', + `("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID' OR "Item"."userId" = ${me.id})` + )}` + }, Number(item.rootId)) + + return root }, invoice: async (item, args, { models }) => { if (item.invoiceId) { diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index 7cd0975c..a59b353a 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -284,6 +284,7 @@ export default { FROM "Earn" WHERE "userId" = $1 AND created_at < $2 + AND (type IS NULL OR type NOT IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL')) GROUP BY "userId", created_at ORDER BY "sortTime" DESC LIMIT ${LIMIT})` @@ -299,6 +300,17 @@ export default { ORDER BY "sortTime" DESC LIMIT ${LIMIT})` ) + queries.push( + `(SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats", + 'ReferralReward' AS type + FROM "Earn" + WHERE "userId" = $1 + AND created_at < $2 + AND type IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL') + GROUP BY "userId", created_at + ORDER BY "sortTime" DESC + LIMIT ${LIMIT})` + ) } if (meFull.noteCowboyHat) { @@ -487,6 +499,22 @@ export default { return null } }, + ReferralReward: { + sources: async (n, args, { me, models }) => { + const [sources] = await models.$queryRawUnsafe(` + SELECT + COALESCE(FLOOR(sum(msats) FILTER(WHERE type = 'FOREVER_REFERRAL') / 1000), 0) AS forever, + COALESCE(FLOOR(sum(msats) FILTER(WHERE type = 'ONE_DAY_REFERRAL') / 1000), 0) AS "oneDay" + FROM "Earn" + WHERE "userId" = $1 AND created_at = $2 + `, Number(me.id), new Date(n.sortTime)) + if (sources.forever + sources.oneDay > 0) { + return sources + } + + return null + } + }, Mention: { mention: async (n, args, { models }) => true, item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me }) diff --git a/api/resolvers/referrals.js b/api/resolvers/referrals.js index 5cbec9f8..b59d4a15 100644 --- a/api/resolvers/referrals.js +++ b/api/resolvers/referrals.js @@ -1,6 +1,6 @@ import { GraphQLError } from 'graphql' -import { withClause, intervalClause } from './growth' import { timeUnitForRange, whenRange } from '@/lib/time' +import { viewGroup } from './growth' export default { Query: { @@ -11,46 +11,18 @@ export default { const range = whenRange(when, from, to) - const [{ totalSats }] = await models.$queryRawUnsafe(` - SELECT COALESCE(FLOOR(sum(msats) / 1000), 0) as "totalSats" - FROM "ReferralAct" - WHERE ${intervalClause(range, 'ReferralAct')} - AND "ReferralAct"."referrerId" = $3 - `, ...range, Number(me.id)) - - const [{ totalReferrals }] = await models.$queryRawUnsafe(` - SELECT count(*)::INTEGER as "totalReferrals" - FROM users - WHERE ${intervalClause(range, 'users')} - AND "referrerId" = $3 - `, ...range, Number(me.id)) - - const stats = await models.$queryRawUnsafe( - `${withClause(range)} - SELECT time, json_build_array( - json_build_object('name', 'referrals', 'value', count(*) FILTER (WHERE act = 'REFERREE')), - json_build_object('name', 'sats', 'value', FLOOR(COALESCE(sum(msats) FILTER (WHERE act IN ('BOOST', 'STREAM', 'FEE')), 0))) + return await models.$queryRawUnsafe(` + SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, + json_build_array( + json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0)), + json_build_object('name', 'one day referrals', 'value', COALESCE(SUM(one_day_referrals), 0)), + json_build_object('name', 'referral sats', 'value', FLOOR(COALESCE(SUM(msats_referrals), 0) / 1000.0)), + json_build_object('name', 'one day referral sats', 'value', FLOOR(COALESCE(SUM(msats_one_day_referrals), 0) / 1000.0)) ) AS data - FROM times - LEFT JOIN - ((SELECT "ReferralAct".created_at, "ReferralAct".msats / 1000.0 as msats, "ItemAct".act::text as act - FROM "ReferralAct" - JOIN "ItemAct" ON "ItemAct".id = "ReferralAct"."itemActId" - WHERE ${intervalClause(range, 'ReferralAct')} - AND "ReferralAct"."referrerId" = $3) - UNION ALL - (SELECT created_at, 0.0 as sats, 'REFERREE' as act - FROM users - WHERE ${intervalClause(range, 'users')} - AND "referrerId" = $3)) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at) - GROUP BY time - ORDER BY time ASC`, ...range, Number(me.id)) - - return { - totalSats, - totalReferrals, - stats - } + FROM ${viewGroup(range, 'user_stats')} + WHERE id = ${me.id} + GROUP BY time + ORDER BY time ASC`, ...range) } } } diff --git a/api/resolvers/user.js b/api/resolvers/user.js index d5dbecc8..034d9b91 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -579,7 +579,8 @@ export default { json_build_object('name', 'comments', 'value', COALESCE(SUM(comments), 0)), json_build_object('name', 'posts', 'value', COALESCE(SUM(posts), 0)), json_build_object('name', 'territories', 'value', COALESCE(SUM(territories), 0)), - json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0)) + json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0)), + json_build_object('name', 'one day referrals', 'value', COALESCE(SUM(one_day_referrals), 0)) ) AS data FROM ${viewGroup(range, 'user_stats')} WHERE id = ${me.id} @@ -594,6 +595,7 @@ export default { json_build_object('name', 'zaps', 'value', ROUND(COALESCE(SUM(msats_tipped), 0) / 1000)), json_build_object('name', 'rewards', 'value', ROUND(COALESCE(SUM(msats_rewards), 0) / 1000)), json_build_object('name', 'referrals', 'value', ROUND( COALESCE(SUM(msats_referrals), 0) / 1000)), + json_build_object('name', 'one day referrals', 'value', ROUND( COALESCE(SUM(msats_one_day_referrals), 0) / 1000)), json_build_object('name', 'territories', 'value', ROUND(COALESCE(SUM(msats_revenue), 0) / 1000)) ) AS data FROM ${viewGroup(range, 'user_stats')} @@ -607,6 +609,7 @@ export default { SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array( json_build_object('name', 'fees', 'value', FLOOR(COALESCE(SUM(msats_fees), 0) / 1000)), + json_build_object('name', 'zapping', 'value', FLOOR(COALESCE(SUM(msats_zaps), 0) / 1000)), json_build_object('name', 'donations', 'value', FLOOR(COALESCE(SUM(msats_donated), 0) / 1000)), json_build_object('name', 'territories', 'value', FLOOR(COALESCE(SUM(msats_billing), 0) / 1000)) ) AS data diff --git a/api/ssrApollo.js b/api/ssrApollo.js index c583be8b..83803bc3 100644 --- a/api/ssrApollo.js +++ b/api/ssrApollo.js @@ -72,7 +72,16 @@ function oneDayReferral (request, { me }) { typeId: String(item.id) }) } else if (referrer.startsWith('profile-')) { - prismaPromise = models.user.findUnique({ where: { name: referrer.slice(8) } }) + const name = referrer.slice(8) + // exclude all pages that are not user profiles + if (['api', 'auth', 'day', 'invites', 'invoices', 'referrals', 'rewards', + 'satistics', 'settings', 'stackers', 'wallet', 'withdrawals', '404', '500', + 'email', 'live', 'login', 'notifications', 'offline', 'search', 'share', + 'signup', 'territory', 'recent', 'top', 'edit', 'post', 'rss', 'saloon', + 'faq', 'story', 'privacy', 'copyright', 'tos', 'changes', 'guide', 'daily', + 'anon', 'ad'].includes(name)) continue + + prismaPromise = models.user.findUnique({ where: { name } }) getData = user => ({ referrerId: user.id, refereeId: parseInt(me.id), diff --git a/api/typeDefs/notifications.js b/api/typeDefs/notifications.js index 269cc9bd..14b44a7b 100644 --- a/api/typeDefs/notifications.js +++ b/api/typeDefs/notifications.js @@ -89,6 +89,19 @@ export default gql` sources: EarnSources } + type ReferralSources { + id: ID! + forever: Int! + oneDay: Int! + } + + type ReferralReward { + id: ID! + earnedSats: Int! + sortTime: Date! + sources: ReferralSources + } + type Revenue { id: ID! earnedSats: Int! @@ -143,6 +156,7 @@ export default gql` | Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral | Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus | TerritoryPost | TerritoryTransfer | Reminder | ItemMention | Invoicification + | ReferralReward type Notifications { lastChecked: Date diff --git a/api/typeDefs/referrals.js b/api/typeDefs/referrals.js index 244d7684..1290aaa9 100644 --- a/api/typeDefs/referrals.js +++ b/api/typeDefs/referrals.js @@ -2,12 +2,6 @@ import { gql } from 'graphql-tag' export default gql` extend type Query { - referrals(when: String, from: String, to: String): Referrals! - } - - type Referrals { - totalSats: Int! - totalReferrals: Int! - stats: [TimeData!]! + referrals(when: String, from: String, to: String): [TimeData!]! } ` diff --git a/awards.csv b/awards.csv index 2573de75..88b7bc8d 100644 --- a/awards.csv +++ b/awards.csv @@ -113,3 +113,5 @@ felipebueno,issue,#1231,#1230,good-first-issue,,,,2k,felipebueno@getalby.com,202 tsmith123,pr,#1223,#107,medium,,2,10k bonus for our slowness,210k,stickymarch60@walletofsatoshi.com,2024-06-22 cointastical,issue,#1223,#107,medium,,2,,20k,cointastical@stacker.news,2024-06-22 kravhen,pr,#1215,#253,medium,,2,upgraded to medium,200k,nichro@getalby.com,2024-06-28 +dillon-co,pr,#1140,#633,hard,,,requested advance,500k,bolt11,2024-07-02 +takitakitanana,issue,,#1257,good-first-issue,,,,2k,takitakitanana@stacker.news,2024-07-11 diff --git a/components/action-tooltip.js b/components/action-tooltip.js index ff5603e1..ecc011a8 100644 --- a/components/action-tooltip.js +++ b/components/action-tooltip.js @@ -15,12 +15,19 @@ export default function ActionTooltip ({ children, notForm, disable, overlayText + {overlayText} } trigger={['hover', 'focus']} show={formik?.isSubmitting ? false : undefined} + popperConfig={{ + modifiers: { + preventOverflow: { + enabled: false + } + } + }} > {children} diff --git a/components/charts.js b/components/charts.js index f8deb618..f9b1deae 100644 --- a/components/charts.js +++ b/components/charts.js @@ -156,7 +156,7 @@ export function WhenComposedChart ({ data, lineNames = [], lineAxis = 'left', areaNames = [], areaAxis = 'left', - barNames = [], barAxis = 'left' + barNames = [], barAxis = 'left', barStackId }) { const router = useRouter() if (!data || data.length === 0) { @@ -189,7 +189,7 @@ export function WhenComposedChart ({ {barNames?.map((v, i) => - )} + )} {areaNames?.map((v, i) => )} {lineNames?.map((v, i) => diff --git a/components/comment.js b/components/comment.js index 7cd079f8..baac8eb0 100644 --- a/components/comment.js +++ b/components/comment.js @@ -77,7 +77,7 @@ export function CommentFlat ({ item, rank, siblingComments, ...props }) { ) :
} { router.push(href, as) }} diff --git a/components/comment.module.css b/components/comment.module.css index ccef5d82..6f24ab61 100644 --- a/components/comment.module.css +++ b/components/comment.module.css @@ -1,13 +1,12 @@ .item { align-items: flex-start; margin-bottom: 0 !important; - padding-bottom: 0 !important; + padding-top: 0 !important; } .upvote { margin-top: 9px; - margin-left: .25rem; - margin-right: 0rem; + padding-right: 0.2rem; } .pin { @@ -65,7 +64,7 @@ .children { margin-top: 0; - margin-left: 30px; + margin-left: 27px; } .comments { @@ -109,7 +108,7 @@ .comment { border-radius: .4rem; padding-top: .5rem; - padding-left: .2rem; + padding-left: .7rem; background-color: var(--theme-commentBg); } @@ -129,7 +128,11 @@ } .comment:not(:first-of-type) { - padding-top: .25rem; + padding-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; +} + +.comment:has(.comment) + .comment{ + padding-top: .5rem; } \ No newline at end of file diff --git a/components/hoverable-popover.js b/components/hoverable-popover.js index 0a0ca4ba..57fa1a36 100644 --- a/components/hoverable-popover.js +++ b/components/hoverable-popover.js @@ -26,11 +26,20 @@ export default function HoverablePopover ({ id, trigger, body, onShow }) { show={showOverlay} placement='bottom' onHide={handleMouseLeave} + popperConfig={{ + modifiers: { + preventOverflow: { + enabled: false + } + } + }} overlay={ {body} diff --git a/components/item-act.js b/components/item-act.js index 5001ab1e..6a720182 100644 --- a/components/item-act.js +++ b/components/item-act.js @@ -8,7 +8,6 @@ import { amountSchema } from '@/lib/validate' import { useToast } from './toast' import { useLightning } from './lightning' import { nextTip } from './upvote' -import { InvoiceCanceledError } from './payment' import { ZAP_UNDO_DELAY_MS } from '@/lib/constants' import { usePaidMutation } from './use-paid-mutation' import { ACT_MUTATION } from '@/fragments/paidAction' @@ -72,7 +71,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal }) } } } - await act({ + const { error } = await act({ variables: { id: item.id, sats: Number(amount), @@ -95,6 +94,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal }) if (!me) setItemMeAnonSats({ id: item.id, amount }) } }) + if (error) throw error addCustomTip(Number(amount)) }, [me, act, down, item.id, onClose, abortSignal, strike]) @@ -219,15 +219,15 @@ export function useZap () { try { await abortSignal.pause({ me, amount: sats }) strike() - await act({ variables, optimisticResponse }) + const { error } = await act({ variables, optimisticResponse }) + if (error) throw error } catch (error) { - if (error instanceof InvoiceCanceledError || error instanceof ActCanceledError) { + if (error instanceof ActCanceledError) { return } const reason = error?.message || error?.toString?.() - - toaster.danger('zap failed: ' + reason) + toaster.danger(reason) } }, [me?.id, strike]) } diff --git a/components/item-info.js b/components/item-info.js index f216acc5..835511b7 100644 --- a/components/item-info.js +++ b/components/item-info.js @@ -24,6 +24,7 @@ import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header' import UserPopover from './user-popover' import { useQrPayment } from './payment' import { useRetryCreateItem } from './use-item-submit' +import { useToast } from './toast' export default function ItemInfo ({ item, full, commentsText = 'comments', @@ -32,6 +33,7 @@ export default function ItemInfo ({ }) { const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000 const me = useMe() + const toaster = useToast() const router = useRouter() const [canEdit, setCanEdit] = useState(item.mine && (Date.now() < editThreshold)) @@ -72,7 +74,14 @@ export default function ItemInfo ({ if (me && item.invoice?.actionState && item.invoice?.actionState !== 'PAID') { if (item.invoice?.actionState === 'FAILED') { Component = () => retry payment - onClick = async () => await retryCreateItem({ variables: { invoiceId: parseInt(item.invoice?.id) } }).catch(console.error) + onClick = async () => { + try { + const { error } = await retryCreateItem({ variables: { invoiceId: parseInt(item.invoice?.id) } }) + if (error) throw error + } catch (error) { + toaster.danger(error.message) + } + } } else { Component = () => ( ) :
} -
+
{item.position && (pinnable || !item.subName) ? : item.meDontLikeSats > item.meSats diff --git a/components/item.module.css b/components/item.module.css index a2a13524..30da4227 100644 --- a/components/item.module.css +++ b/components/item.module.css @@ -118,7 +118,7 @@ a.link:visited { display: flex; justify-content: flex-start; min-width: 0; - padding-bottom: .5rem; + padding-top: .5rem; } .item .companyImage { @@ -169,7 +169,8 @@ a.link:visited { } .children { - margin-left: 28px; + margin-left: 27px; + padding-top: .5rem; } .rank { diff --git a/components/items.js b/components/items.js index dce714c7..6a2064c7 100644 --- a/components/items.js +++ b/components/items.js @@ -51,7 +51,7 @@ export default function Items ({ ssrData, variables = {}, query, destructureData <>
{itemsWithPins.filter(filter).map((item, i) => ( - 0} /> + 0} /> ))}
* { diff --git a/components/more-footer.js b/components/more-footer.js index 8630425b..9675b03e 100644 --- a/components/more-footer.js +++ b/components/more-footer.js @@ -33,7 +33,7 @@ export default function MoreFooter ({ cursor, count, fetchMore, Skeleton, invisi ) } - return
+ return
} export function NavigateFooter ({ cursor, count, fetchMore, href, text, invisible, noMoreText = 'NO MORE' }) { diff --git a/components/nav/desktop/second-bar.js b/components/nav/desktop/second-bar.js index 8b0395da..4120c306 100644 --- a/components/nav/desktop/second-bar.js +++ b/components/nav/desktop/second-bar.js @@ -6,7 +6,7 @@ export default function SecondBar (props) { const { prefix, topNavKey, sub } = props if (!hasNavSelect(props)) return null return ( - +