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/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/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/notifications.js b/components/notifications.js index 5f4e312b..6f899808 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -10,6 +10,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 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 BaldIcon from '@/svgs/bald.svg' @@ -62,7 +63,8 @@ function Notification ({ n, fresh }) { (type === 'TerritoryPost' && ) || (type === 'TerritoryTransfer' && ) || (type === 'Reminder' && ) || - (type === 'Invoicification' && ) + (type === 'Invoicification' && ) || + (type === 'ReferralReward' && ) } ) @@ -132,6 +134,7 @@ const defaultOnClick = n => { if (type === 'Invoicification') return itemLink(n.invoice.item) if (type === 'WithdrawlPaid') return { href: `/withdrawals/${n.id}` } if (type === 'Referral') return { href: '/referrals/month' } + if (type === 'ReferralReward') return { href: '/referrals/month' } if (type === 'Streak') return {} if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` } @@ -172,7 +175,7 @@ function EarnNotification ({ n }) { return (
-
+
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in rewards{time}
@@ -184,7 +187,29 @@ function EarnNotification ({ n }) { {n.sources.tipComments > 0 && {(n.sources.comments > 0 || n.sources.posts > 0 || n.sources.tipPosts > 0) && ' \\ '}{numWithUnits(n.sources.tipComments, { abbreviate: false })} for zapping top comments early}
}
- SN distributes the sats it earns back to its best stackers. These sats come from jobs, boosts, posting fees, and donations. You can see the rewards pool and make a donation here. + SN distributes the sats it earns to top stackers like you daily. The top stackers make the top posts and comments or zap the top posts and comments early and generously. View the rewards pool and make a donation here. +
+ click for details +
+
+ ) +} + +function ReferralReward ({ n }) { + return ( +
+ +
+
+ you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in referral rewards{dayMonthYear(new Date(n.sortTime))} +
+ {n.sources && +
+ {n.sources.forever > 0 && {numWithUnits(n.sources.forever, { abbreviate: false })} for stackers joining because of you} + {n.sources.oneDay > 0 && {n.sources.oneDay > 0 && ' \\ '}{numWithUnits(n.sources.oneDay, { abbreviate: false })} for stackers referred to content by you today} +
} +
+ SN gives referral rewards to stackers like you for referring the top stackers daily. You refer stackers when they visit your posts, comments, profile, territory, or if they visit SN through your referral links.
click for details
@@ -433,8 +458,8 @@ function WithdrawlPaid ({ n }) { function Referral ({ n }) { return ( - - someone joined via one of your referral links + + someone joined SN because of you {timeSince(new Date(n.sortTime))} ) diff --git a/fragments/notifications.js b/fragments/notifications.js index 0c9fa843..d43f3dc1 100644 --- a/fragments/notifications.js +++ b/fragments/notifications.js @@ -99,6 +99,15 @@ export const NOTIFICATIONS = gql` tipComments } } + ... on ReferralReward { + id + sortTime + earnedSats + sources { + forever + oneDay + } + } ... on Referral { id sortTime diff --git a/pages/referrals/[when].js b/pages/referrals/[when].js index 4e8cabcd..99f48be7 100644 --- a/pages/referrals/[when].js +++ b/pages/referrals/[when].js @@ -21,14 +21,10 @@ const REFERRALS = gql` query Referrals($when: String!, $from: String, $to: String) { referrals(when: $when, from: $from, to: $to) { - totalSats - totalReferrals - stats { - time - data { - name - value - } + time + data { + name + value } } }` @@ -54,7 +50,12 @@ export default function Referrals ({ ssrData }) { const { data } = useQuery(REFERRALS, { variables: { when: router.query.when, from: router.query.from, to: router.query.to } }) if (!data && !ssrData) return - const { referrals: { totalSats, totalReferrals, stats } } = data || ssrData + const { referrals } = data || ssrData + const totalSats = referrals.reduce( + (total, a) => total + a.data?.filter(d => d.name.endsWith('sats')).reduce( + (acc, d) => acc + d.value, + 0), + 0) const when = router.query.when @@ -62,7 +63,7 @@ export default function Referrals ({ ssrData }) {

- {numWithUnits(totalReferrals, { unitPlural: 'referrals', unitSingular: 'referral' })} & {numWithUnits(totalSats, { abbreviate: false })} in the last + {numWithUnits(totalSats, { abbreviate: false })} in the last