From 439c83f975fa6d65437391de490fd06e148a8a1f Mon Sep 17 00:00:00 2001 From: keyan Date: Tue, 6 Dec 2022 15:23:45 -0600 Subject: [PATCH 01/11] all things being equal, sort comments by sats --- api/resolvers/item.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 3f0888eb..d963875f 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -14,13 +14,13 @@ async function comments (me, models, id, sort) { let orderBy switch (sort) { case 'top': - orderBy = `ORDER BY ${await orderByNumerator(me, models)} DESC, "Item".id DESC` + orderBy = `ORDER BY ${await orderByNumerator(me, models)} DESC, "Item".msats DESC, "Item".id DESC` break case 'recent': - orderBy = 'ORDER BY "Item".created_at DESC, "Item".id DESC' + orderBy = 'ORDER BY "Item".created_at DESC, "Item".msats DESC, "Item".id DESC' break default: - orderBy = `ORDER BY ${await orderByNumerator(me, models)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".id DESC` + orderBy = `ORDER BY ${await orderByNumerator(me, models)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, "Item".id DESC` break } From e1bdb9c7691d00ea2193ea0e9b46bf22f1260528 Mon Sep 17 00:00:00 2001 From: keyan Date: Wed, 7 Dec 2022 18:04:02 -0600 Subject: [PATCH 02/11] donations to rewards --- api/resolvers/growth.js | 31 +++- api/resolvers/index.js | 3 +- api/resolvers/rewards.js | 48 ++++++ api/resolvers/user.js | 18 ++- api/resolvers/wallet.js | 10 ++ api/typeDefs/index.js | 3 +- api/typeDefs/rewards.js | 16 ++ components/footer.js | 7 + components/layout-center.js | 4 +- pages/rewards.js | 142 ++++++++++++++++++ pages/satistics.js | 11 ++ pages/users/[when].js | 3 +- .../20221206213226_donate/migration.sql | 39 +++++ .../20221207212053_donate_func/migration.sql | 25 +++ prisma/schema.prisma | 10 ++ worker/earn.js | 10 +- 16 files changed, 360 insertions(+), 20 deletions(-) create mode 100644 api/resolvers/rewards.js create mode 100644 api/typeDefs/rewards.js create mode 100644 pages/rewards.js create mode 100644 prisma/migrations/20221206213226_donate/migration.sql create mode 100644 prisma/migrations/20221207212053_donate_func/migration.sql diff --git a/api/resolvers/growth.js b/api/resolvers/growth.js index 18a78b1f..f048d109 100644 --- a/api/resolvers/growth.js +++ b/api/resolvers/growth.js @@ -74,10 +74,18 @@ export default { json_build_object('name', 'jobs', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'STREAM')), json_build_object('name', 'boost', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'BOOST')), json_build_object('name', 'fees', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'FEE')), - json_build_object('name', 'tips', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'TIP')) + json_build_object('name', 'tips', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'TIP')), + json_build_object('name', 'donation', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'DONATION')) ) AS data FROM times - LEFT JOIN "ItemAct" ON ${intervalClause(when, 'ItemAct', true)} time = date_trunc('${timeUnit(when)}', created_at) + LEFT JOIN + ((SELECT "ItemAct".created_at, "userId", act::text as act + FROM "ItemAct" + WHERE ${intervalClause(when, 'ItemAct', false)}) + UNION ALL + (SELECT created_at, "userId", 'DONATION' as act + FROM "Donation" + WHERE ${intervalClause(when, 'Donation', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at) GROUP BY time ORDER BY time ASC`) }, @@ -98,14 +106,21 @@ export default { return await models.$queryRaw( `${withClause(when)} SELECT time, json_build_array( - json_build_object('name', 'jobs', 'value', coalesce(floor(sum(CASE WHEN act = 'STREAM' THEN "ItemAct".msats ELSE 0 END)/1000),0)), - json_build_object('name', 'boost', 'value', coalesce(floor(sum(CASE WHEN act = 'BOOST' THEN "ItemAct".msats ELSE 0 END)/1000),0)), - json_build_object('name', 'fees', 'value', coalesce(floor(sum(CASE WHEN act NOT IN ('BOOST', 'TIP', 'STREAM') THEN "ItemAct".msats ELSE 0 END)/1000),0)), - json_build_object('name', 'tips', 'value', coalesce(floor(sum(CASE WHEN act = 'TIP' THEN "ItemAct".msats ELSE 0 END)/1000),0)) + json_build_object('name', 'jobs', 'value', coalesce(floor(sum(CASE WHEN act = 'STREAM' THEN msats ELSE 0 END)/1000),0)), + json_build_object('name', 'boost', 'value', coalesce(floor(sum(CASE WHEN act = 'BOOST' THEN msats ELSE 0 END)/1000),0)), + json_build_object('name', 'fees', 'value', coalesce(floor(sum(CASE WHEN act NOT IN ('BOOST', 'TIP', 'STREAM', 'DONATION') THEN msats ELSE 0 END)/1000),0)), + json_build_object('name', 'tips', 'value', coalesce(floor(sum(CASE WHEN act = 'TIP' THEN msats ELSE 0 END)/1000),0)), + json_build_object('name', 'donations', 'value', coalesce(floor(sum(CASE WHEN act = 'DONATION' THEN msats ELSE 0 END)/1000),0)) ) AS data FROM times - LEFT JOIN "ItemAct" ON ${intervalClause(when, 'ItemAct', true)} time = date_trunc('${timeUnit(when)}', created_at) - JOIN "Item" ON "ItemAct"."itemId" = "Item".id + LEFT JOIN + ((SELECT "ItemAct".created_at, msats, act::text as act + FROM "ItemAct" + WHERE ${intervalClause(when, 'ItemAct', false)}) + UNION ALL + (SELECT created_at, sats * 1000 as msats, 'DONATION' as act + FROM "Donation" + WHERE ${intervalClause(when, 'Donation', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at) GROUP BY time ORDER BY time ASC`) }, diff --git a/api/resolvers/index.js b/api/resolvers/index.js index d2bb87d9..7798a789 100644 --- a/api/resolvers/index.js +++ b/api/resolvers/index.js @@ -9,7 +9,8 @@ import sub from './sub' import upload from './upload' import growth from './growth' import search from './search' +import rewards from './rewards' import { GraphQLJSONObject } from 'graphql-type-json' export default [user, item, message, wallet, lnurl, notifications, invite, sub, - upload, growth, search, { JSONObject: GraphQLJSONObject }] + upload, growth, search, rewards, { JSONObject: GraphQLJSONObject }] diff --git a/api/resolvers/rewards.js b/api/resolvers/rewards.js new file mode 100644 index 00000000..55e16cfd --- /dev/null +++ b/api/resolvers/rewards.js @@ -0,0 +1,48 @@ +import { AuthenticationError } from 'apollo-server-micro' +import serialize from './serial' + +export default { + Query: { + expectedRewards: async (parent, args, { models }) => { + // get the last reward time, then get all contributions to rewards since then + const lastReward = await models.earn.findFirst({ + orderBy: { + createdAt: 'desc' + } + }) + + const [result] = await models.$queryRaw` + SELECT coalesce(sum(sats), 0) as total, json_build_array( + json_build_object('name', 'donations', 'value', coalesce(sum(sats) FILTER(WHERE type = 'DONATION'), 0)), + json_build_object('name', 'fees', 'value', coalesce(sum(sats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM')), 0)), + json_build_object('name', 'boost', 'value', coalesce(sum(sats) FILTER(WHERE type = 'BOOST'), 0)), + json_build_object('name', 'jobs', 'value', coalesce(sum(sats) FILTER(WHERE type = 'STREAM'), 0)) + ) AS sources + FROM ( + (SELECT msats / 1000 as sats, act::text as type + FROM "ItemAct" + WHERE created_at > ${lastReward.createdAt} AND "ItemAct".act <> 'TIP') + UNION ALL + (SELECT sats, 'DONATION' as type + FROM "Donation" + WHERE created_at > ${lastReward.createdAt}) + ) subquery` + + return result + } + }, + Mutation: { + donateToRewards: async (parent, { sats }, { me, models }) => { + if (!me) { + throw new AuthenticationError('you must be logged in') + } + + await serialize(models, + models.$queryRaw( + 'SELECT donate($1, $2)', + sats, Number(me.id))) + + return sats + } + } +} diff --git a/api/resolvers/user.js b/api/resolvers/user.js index a120ec3a..3485189d 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -93,12 +93,20 @@ export default { let users if (sort === 'spent') { users = await models.$queryRaw(` - SELECT users.*, floor(sum("ItemAct".msats)/1000) as spent - FROM "ItemAct" - JOIN users on "ItemAct"."userId" = users.id - WHERE "ItemAct".created_at <= $1 + SELECT users.*, sum(sats_spent) as spent + FROM + ((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent + FROM "ItemAct" + WHERE "ItemAct".created_at <= $1 + ${within('ItemAct', when)} + GROUP BY "userId") + UNION ALL + (SELECT "userId", sats as sats_spent + FROM "Donation" + WHERE created_at <= $1 + ${within('Donation', when)})) spending + JOIN users on spending."userId" = users.id AND NOT users."hideFromTopUsers" - ${within('ItemAct', when)} GROUP BY users.id, users.name ORDER BY spent DESC NULLS LAST, users.created_at DESC OFFSET $2 diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index b8dafcec..3255b230 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -122,6 +122,13 @@ export default { WHERE "ItemAct"."userId" = $1 AND "ItemAct".created_at <= $2 GROUP BY "Item".id)`) + queries.push( + `(SELECT ('donation' || "Donation".id) as id, "Donation".id as "factId", NULL as bolt11, + created_at as "createdAt", sats * 1000 as msats, + 0 as "msatsFee", NULL as status, 'donation' as type + FROM "Donation" + WHERE "userId" = $1 + AND created_at <= $2)`) } if (queries.length === 0) { @@ -157,6 +164,9 @@ export default { case 'spent': f.msats *= -1 break + case 'donation': + f.msats *= -1 + break default: break } diff --git a/api/typeDefs/index.js b/api/typeDefs/index.js index 5768cc88..183069da 100644 --- a/api/typeDefs/index.js +++ b/api/typeDefs/index.js @@ -10,6 +10,7 @@ import invite from './invite' import sub from './sub' import upload from './upload' import growth from './growth' +import rewards from './rewards' const link = gql` type Query { @@ -26,4 +27,4 @@ const link = gql` ` export default [link, user, item, message, wallet, lnurl, notifications, invite, - sub, upload, growth] + sub, upload, growth, rewards] diff --git a/api/typeDefs/rewards.js b/api/typeDefs/rewards.js new file mode 100644 index 00000000..e9776c20 --- /dev/null +++ b/api/typeDefs/rewards.js @@ -0,0 +1,16 @@ +import { gql } from 'apollo-server-micro' + +export default gql` + extend type Query { + expectedRewards: ExpectedRewards! + } + + extend type Mutation { + donateToRewards(sats: Int!): Int! + } + + type ExpectedRewards { + total: Int! + sources: [NameValue!]! + } +` diff --git a/components/footer.js b/components/footer.js index 7b013e68..f3cb2377 100644 --- a/components/footer.js +++ b/components/footer.js @@ -151,6 +151,13 @@ export default function Footer ({ noLinks }) { darkMode.toggle()} className='fill-grey theme' /> } +
+ + + rewards + + +
diff --git a/components/layout-center.js b/components/layout-center.js index c326de15..41b3d769 100644 --- a/components/layout-center.js +++ b/components/layout-center.js @@ -1,10 +1,10 @@ import Layout from './layout' import styles from './layout-center.module.css' -export default function LayoutCenter ({ children, ...props }) { +export default function LayoutCenter ({ children, footerLinks, ...props }) { return (
- +
{children}
diff --git a/pages/rewards.js b/pages/rewards.js new file mode 100644 index 00000000..17a44da3 --- /dev/null +++ b/pages/rewards.js @@ -0,0 +1,142 @@ +import { gql } from 'apollo-server-micro' +import { useEffect, useRef, useState } from 'react' +import { Button, InputGroup, Modal } from 'react-bootstrap' +import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts' +import { getGetServerSideProps } from '../api/ssrApollo' +import { Form, Input, SubmitButton } from '../components/form' +import LayoutCenter from '../components/layout-center' +import * as Yup from 'yup' +import { useMutation, useQuery } from '@apollo/client' +import Link from 'next/link' + +const REWARDS = gql` +{ + expectedRewards { + total + sources { + name + value + } + } +} +` + +export const getServerSideProps = getGetServerSideProps(REWARDS) + +export default function Rewards ({ data: { expectedRewards: { total, sources } } }) { + const { data } = useQuery(REWARDS, { pollInterval: 1000 }) + + if (data) { + ({ expectedRewards: { total, sources } } = data) + } + + return ( + +

+
{total} sats to be rewarded today
+ + learn about rewards + +

+
+ +
+ +
+ ) +} + +const COLORS = [ + 'var(--secondary)', + 'var(--info)', + 'var(--success)', + 'var(--boost)', + 'var(--grey)' +] + +function GrowthPieChart ({ data }) { + return ( + + + + { + data.map((entry, index) => ( + + )) + } + + + + + ) +} + +export const DonateSchema = Yup.object({ + amount: Yup.number().typeError('must be a number').required('required') + .positive('must be positive').integer('must be whole') +}) + +export function DonateButton () { + const [show, setShow] = useState(false) + const inputRef = useRef(null) + const [donateToRewards] = useMutation( + gql` + mutation donateToRewards($sats: Int!) { + donateToRewards(sats: $sats) + }`) + + useEffect(() => { + inputRef.current?.focus() + }, [show]) + + return ( + <> + + { + setShow(false) + }} + > +
setShow(false)}>X
+ +
{ + await donateToRewards({ + variables: { + sats: Number(amount) + } + }) + setShow(false) + }} + > + sats} + /> +
+ donate +
+
+
+
+ + ) +} diff --git a/pages/satistics.js b/pages/satistics.js index 97cbacea..8cdf4f14 100644 --- a/pages/satistics.js +++ b/pages/satistics.js @@ -98,6 +98,16 @@ function Detail ({ fact }) { ) } + if (fact.type === 'donation') { + return ( + <> +
+ You made a donation to daily rewards! +
+ + ) + } + if (!fact.item) { return ( <> @@ -145,6 +155,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor case 'invoice': return `/${fact.type}s/${fact.factId}` case 'earn': + case 'donation': return default: return `/items/${fact.factId}` diff --git a/pages/users/[when].js b/pages/users/[when].js index 976df325..9a19c700 100644 --- a/pages/users/[when].js +++ b/pages/users/[when].js @@ -140,7 +140,8 @@ const COLORS = [ 'var(--info)', 'var(--success)', 'var(--boost)', - 'var(--theme-grey)' + 'var(--theme-grey)', + 'var(--danger)' ] function GrowthAreaChart ({ data }) { diff --git a/prisma/migrations/20221206213226_donate/migration.sql b/prisma/migrations/20221206213226_donate/migration.sql new file mode 100644 index 00000000..d0f37e7a --- /dev/null +++ b/prisma/migrations/20221206213226_donate/migration.sql @@ -0,0 +1,39 @@ +-- CreateTable +CREATE TABLE "Donation" ( + "id" SERIAL NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "sats" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + + PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Donation" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE OR REPLACE FUNCTION donate(sats INTEGER, user_id INTEGER) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + user_sats INTEGER; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + SELECT msats / 1000 + INTO user_sats + FROM users WHERE id = user_id; + + IF sats > user_sats THEN + RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS'; + END IF; + + UPDATE users SET msats = msats - (sats * 1000) WHERE id = user_id; + + INSERT INTO "Donate" (sats, "userId", created_at, updated_at) + VALUES (sats, user_id, now_utc(), now_utc()); + + RETURN sats; +END; +$$; \ No newline at end of file diff --git a/prisma/migrations/20221207212053_donate_func/migration.sql b/prisma/migrations/20221207212053_donate_func/migration.sql new file mode 100644 index 00000000..cdd50f4c --- /dev/null +++ b/prisma/migrations/20221207212053_donate_func/migration.sql @@ -0,0 +1,25 @@ +CREATE OR REPLACE FUNCTION donate(sats INTEGER, user_id INTEGER) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + user_sats INTEGER; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + SELECT msats / 1000 + INTO user_sats + FROM users WHERE id = user_id; + + IF sats > user_sats THEN + RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS'; + END IF; + + UPDATE users SET msats = msats - (sats * 1000) WHERE id = user_id; + + INSERT INTO "Donate" (sats, "userId", created_at, updated_at) + VALUES (sats, user_id, now_utc(), now_utc()); + + RETURN sats; +END; +$$; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1d620f50..062292b1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -68,12 +68,22 @@ model User { Earn Earn[] Upload Upload[] @relation(name: "Uploads") PollVote PollVote[] + Donation Donation[] @@index([createdAt]) @@index([inviteId]) @@map(name: "users") } +model Donation { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at") + sats Int + userId Int + user User @relation(fields: [userId], references: [id]) +} + model Upload { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) @map(name: "created_at") diff --git a/worker/earn.js b/worker/earn.js index 1a87a330..4b07a2cb 100644 --- a/worker/earn.js +++ b/worker/earn.js @@ -10,13 +10,19 @@ function earn ({ models }) { console.log('running', name) // compute how much sn earned today - const [{ sum }] = await models.$queryRaw` - SELECT sum("ItemAct".msats) + let [{ sum }] = await models.$queryRaw` + SELECT coalesce(sum("ItemAct".msats), 0) as sum FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id WHERE "ItemAct".act <> 'TIP' AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'` + const [{ sum: donatedSum }] = await models.$queryRaw` + SELECT coalesce(sum(sats), 0) as sum + FROM "Donation" + WHERE created_at > now_utc() - INTERVAL '1 day'` + sum += donatedSum * 1000 + /* How earnings work: 1/3: top 21% posts over last 36 hours, scored on a relative basis From 7b7ed0047cf0f2880e46fa1d2e98d6880a45c353 Mon Sep 17 00:00:00 2001 From: keyan Date: Fri, 9 Dec 2022 13:25:38 -0600 Subject: [PATCH 03/11] turbo tipping --- api/typeDefs/user.js | 5 +-- components/upvote.js | 20 +++++++---- fragments/users.js | 10 +++--- pages/settings.js | 36 +++++++++++++++++++ .../migration.sql | 2 ++ prisma/schema.prisma | 5 ++- 6 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 prisma/migrations/20221208224752_turbo_tipping/migration.sql diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 1fe2bdf0..e79a4722 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -19,8 +19,8 @@ export default gql` extend type Mutation { setName(name: String!): Boolean - setSettings(tipDefault: Int!, fiatCurrency: String!, noteItemSats: Boolean!, noteEarning: Boolean!, - noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!, + setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, noteItemSats: Boolean!, + noteEarning: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!, noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!, hideFromTopUsers: Boolean!, wildWestMode: Boolean!, greeterMode: Boolean!): User setPhoto(photoId: ID!): Int! @@ -49,6 +49,7 @@ export default gql` freeComments: Int! hasInvites: Boolean! tipDefault: Int! + turboTipping: Boolean! fiatCurrency: String! bio: Item bioId: Int diff --git a/components/upvote.js b/components/upvote.js index f1c44404..f2ef5190 100644 --- a/components/upvote.js +++ b/components/upvote.js @@ -152,11 +152,19 @@ export default function UpVote ({ item, className }) { } ) - const overlayText = () => { - if (me?.tipDefault) { - return `${me.tipDefault} sat${me.tipDefault > 1 ? 's' : ''}` + // what should our next tip be? + let sats = me?.tipDefault || 1 + if (me?.turboTipping && item?.meSats) { + let raiseTip = sats + while (item?.meSats >= raiseTip) { + raiseTip *= 10 } - return '1 sat' + + sats = raiseTip - item.meSats + } + + const overlayText = () => { + return `${sats} sat${sats > 1 ? 's' : ''}` } const color = getColor(item?.meSats) @@ -196,11 +204,11 @@ export default function UpVote ({ item, className }) { try { await act({ - variables: { id: item.id, sats: me.tipDefault || 1 }, + variables: { id: item.id, sats }, optimisticResponse: { act: { id: `Item:${item.id}`, - sats: me.tipDefault || 1, + sats, vote: 0 } } diff --git a/fragments/users.js b/fragments/users.js index 8fed0d9f..0458ddc7 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -12,6 +12,7 @@ export const ME = gql` freePosts freeComments tipDefault + turboTipping fiatCurrency bioId upvotePopover @@ -34,6 +35,7 @@ export const ME = gql` export const SETTINGS_FIELDS = gql` fragment SettingsFields on User { tipDefault + turboTipping fiatCurrency noteItemSats noteEarning @@ -65,12 +67,12 @@ ${SETTINGS_FIELDS} export const SET_SETTINGS = gql` ${SETTINGS_FIELDS} -mutation setSettings($tipDefault: Int!, $fiatCurrency: String!, $noteItemSats: Boolean!, $noteEarning: Boolean!, - $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!, +mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $noteItemSats: Boolean!, + $noteEarning: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!, $noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!, $hideFromTopUsers: Boolean!, $wildWestMode: Boolean!, $greeterMode: Boolean!) { - setSettings(tipDefault: $tipDefault, fiatCurrency: $fiatCurrency, noteItemSats: $noteItemSats, - noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants, + setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency, + noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants, noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites, noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers, wildWestMode: $wildWestMode, greeterMode: $greeterMode) { diff --git a/pages/settings.js b/pages/settings.js index 41c75073..37793fc9 100644 --- a/pages/settings.js +++ b/pages/settings.js @@ -14,6 +14,7 @@ import { useRouter } from 'next/router' import Info from '../components/info' import { CURRENCY_SYMBOLS } from '../components/price' import Link from 'next/link' +import AccordianItem from '../components/accordian-item' export const getServerSideProps = getGetServerSideProps(SETTINGS) @@ -59,6 +60,7 @@ export default function Settings ({ data: { settings } }) {
sats} + hint={note: you can also press and hold the lightning bolt to tip custom amounts} /> +
+ advanced
} + body={turbo tipping + +
    +
  • Makes every additional bolt click raise your total tip to another 10x multiple of your default tip
  • +
  • e.g. if your tip default is 10 sats +
      +
    • 1st click: 10 sats total tipped
    • +
    • 2nd click: 100 sats total tipped
    • +
    • 3rd click: 1000 sats total tipped
    • +
    • 4th click: 10000 sats total tipped
    • +
    • and so on ...
    • +
    +
  • +
  • You can still custom tip via long press +
      +
    • the next bolt click rounds up to the next greatest 10x multiple of your default
    • +
    +
  • +
+
+
+ } + />} + /> +
router.push(`/referrals/${e.target.value}`)} + /> + + + + +
+
referral link:
+ +
+
    +
  • {`appending /r/${me.name} to any SN link makes it a ref link`} +
      +
    • e.g. https://stacker.news/items/1/r/{me.name}
    • +
    +
  • +
  • earn 21% of boost and job fees spent by referred stackers
  • +
  • earn 2.1% of all tips received by referred stackers
  • +
  • invite links are also implicitly referral links
  • +
+ + ) +} diff --git a/pages/satistics.js b/pages/satistics.js index 8cdf4f14..771c1d4c 100644 --- a/pages/satistics.js +++ b/pages/satistics.js @@ -15,7 +15,6 @@ import { useRouter } from 'next/router' import Item from '../components/item' import Comment from '../components/comment' import React from 'react' -import Info from '../components/info' export const getServerSideProps = getGetServerSideProps(WALLET_HISTORY) @@ -102,7 +101,16 @@ function Detail ({ fact }) { return ( <>
- You made a donation to daily rewards! + You made a donation to daily rewards! +
+ + ) + } + if (fact.type === 'referral') { + return ( + <> +
+ You stacked sats from a referral!
) @@ -156,6 +164,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor return `/${fact.type}s/${fact.factId}` case 'earn': case 'donation': + case 'referral': return default: return `/items/${fact.factId}` @@ -212,11 +221,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor type detail -
sats - -
Sats are rounded down from millisats to the nearest sat, so the actual amount might be slightly larger.
-
-
+ sats @@ -231,7 +236,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor - {Math.floor(f.sats)} + {f.sats} ) diff --git a/pages/settings.js b/pages/settings.js index 37793fc9..e955dcf1 100644 --- a/pages/settings.js +++ b/pages/settings.js @@ -146,7 +146,7 @@ export default function Settings ({ data: { settings } }) { groupClassName='mb-0' /> diff --git a/pages/users/[when].js b/pages/users/[when].js index 9a19c700..6a3db864 100644 --- a/pages/users/[when].js +++ b/pages/users/[when].js @@ -1,11 +1,9 @@ import { gql } from '@apollo/client' import { getGetServerSideProps } from '../../api/ssrApollo' import Layout from '../../components/layout' -import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts' import { Col, Row } from 'react-bootstrap' -import { abbrNum } from '../../lib/format' import { UsageHeader } from '../../components/usage-header' -import { useRouter } from 'next/router' +import { WhenLineChart, WhenAreaChart } from '../../components/when-charts' export const getServerSideProps = getGetServerSideProps( gql` @@ -55,46 +53,6 @@ export const getServerSideProps = getGetServerSideProps( } }`) -// todo: this needs to accomodate hours, days, months now -const dateFormatter = when => { - return timeStr => { - const date = new Date(timeStr) - switch (when) { - case 'week': - case 'month': - return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${date.getUTCDate()}` - case 'year': - case 'forever': - return `${('0' + (date.getUTCMonth() % 12 + 1)).slice(-2)}/${String(date.getUTCFullYear()).slice(-2)}` - default: - return `${date.getHours() % 12 || 12}${date.getHours() >= 12 ? 'pm' : 'am'}` - } - } -} - -function xAxisName (when) { - switch (when) { - case 'week': - case 'month': - return 'days' - case 'year': - case 'forever': - return 'months' - default: - return 'hours' - } -} - -const transformData = data => { - return data.map(entry => { - const obj = { time: entry.time } - entry.data.forEach(entry1 => { - obj[entry1.name] = entry1.value - }) - return obj - }) -} - export default function Growth ({ data: { registrationGrowth, itemGrowth, spendingGrowth, spenderGrowth, stackingGrowth, stackerGrowth } }) { @@ -104,112 +62,33 @@ export default function Growth ({
stackers
- +
stacking
- +
spenders
- +
spending
- +
registrations
- +
items
- +
) } - -const COLORS = [ - 'var(--secondary)', - 'var(--info)', - 'var(--success)', - 'var(--boost)', - 'var(--theme-grey)', - 'var(--danger)' -] - -function GrowthAreaChart ({ data }) { - const router = useRouter() - if (!data || data.length === 0) { - return null - } - // transform data into expected shape - data = transformData(data) - // need to grab when - const when = router.query.when - - return ( - - - - - - - {Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) => - )} - - - ) -} - -function GrowthLineChart ({ data }) { - const router = useRouter() - if (!data || data.length === 0) { - return null - } - // transform data into expected shape - data = transformData(data) - // need to grab when - const when = router.query.when - - return ( - - - - - - - {Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) => - )} - - - ) -} diff --git a/prisma/migrations/20221213220945_referral/migration.sql b/prisma/migrations/20221213220945_referral/migration.sql new file mode 100644 index 00000000..70d474d7 --- /dev/null +++ b/prisma/migrations/20221213220945_referral/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "referrerId" INTEGER; + +-- AddForeignKey +ALTER TABLE "users" ADD FOREIGN KEY ("referrerId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20221214175911_referral_act/migration.sql b/prisma/migrations/20221214175911_referral_act/migration.sql new file mode 100644 index 00000000..46b01055 --- /dev/null +++ b/prisma/migrations/20221214175911_referral_act/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "ReferralAct" ( + "id" SERIAL NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "referrerId" INTEGER NOT NULL, + "itemActId" INTEGER NOT NULL, + "msats" BIGINT NOT NULL, + + PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "ReferralAct" ADD FOREIGN KEY ("referrerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ReferralAct" ADD FOREIGN KEY ("itemActId") REFERENCES "ItemAct"("id") ON DELETE CASCADE ON UPDATE CASCADE; \ No newline at end of file diff --git a/prisma/migrations/20221214201527_referral_funcs/migration.sql b/prisma/migrations/20221214201527_referral_funcs/migration.sql new file mode 100644 index 00000000..2afdf709 --- /dev/null +++ b/prisma/migrations/20221214201527_referral_funcs/migration.sql @@ -0,0 +1,202 @@ +CREATE OR REPLACE FUNCTION referral_act(referrer_id INTEGER, item_act_id INTEGER) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + act_msats BIGINT; + referral_act "ItemActType"; + referral_msats BIGINT; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + SELECT msats, act INTO act_msats, referral_act FROM "ItemAct" WHERE id = item_act_id; + + IF referral_act IN ('FEE', 'BOOST', 'STREAM') THEN + referral_msats := CEIL(act_msats * .21); + INSERT INTO "ReferralAct" ("referrerId", "itemActId", msats, created_at, updated_at) + VALUES(referrer_id, item_act_id, referral_msats, now_utc(), now_utc()); + UPDATE users + SET msats = msats + referral_msats, "stackedMsats" = "stackedMsats" + referral_msats + WHERE id = referrer_id; + END IF; + + RETURN 0; +END; +$$; + +-- add referral act on item_act +CREATE OR REPLACE FUNCTION item_act(item_id INTEGER, user_id INTEGER, act "ItemActType", act_sats INTEGER) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + user_msats BIGINT; + act_msats BIGINT; + fee_msats BIGINT; + item_act_id INTEGER; + referrer_id INTEGER; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + act_msats := act_sats * 1000; + SELECT msats, "referrerId" INTO user_msats, referrer_id FROM users WHERE id = user_id; + IF act_msats > user_msats THEN + RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS'; + END IF; + + -- deduct msats from actor + UPDATE users SET msats = msats - act_msats WHERE id = user_id; + + IF act = 'VOTE' THEN + RAISE EXCEPTION 'SN_UNSUPPORTED'; + END IF; + + IF act = 'TIP' THEN + -- call to influence weightedVotes ... we need to do this before we record the acts because + -- the priors acts are taken into account + PERFORM weighted_votes_after_tip(item_id, user_id, act_sats); + + -- take 10% and insert as FEE + fee_msats := CEIL(act_msats * 0.1); + act_msats := act_msats - fee_msats; + + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (fee_msats, item_id, user_id, 'FEE', now_utc(), now_utc()) + RETURNING id INTO item_act_id; + + -- add sats to actee's balance and stacked count + UPDATE users + SET msats = msats + act_msats, "stackedMsats" = "stackedMsats" + act_msats + WHERE id = (SELECT COALESCE("fwdUserId", "userId") FROM "Item" WHERE id = item_id) + RETURNING "referrerId" INTO referrer_id; + + -- leave the rest as a tip + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (act_msats, item_id, user_id, 'TIP', now_utc(), now_utc()); + + -- call to denormalize sats and commentSats + PERFORM sats_after_tip(item_id, user_id, act_msats + fee_msats); + ELSE -- BOOST, POLL, DONT_LIKE_THIS, STREAM + -- call to influence if DONT_LIKE_THIS weightedDownVotes + IF act = 'DONT_LIKE_THIS' THEN + -- make sure they haven't done this before + IF EXISTS (SELECT 1 FROM "ItemAct" WHERE "itemId" = item_id AND "userId" = user_id AND "ItemAct".act = 'DONT_LIKE_THIS') THEN + RAISE EXCEPTION 'SN_DUPLICATE'; + END IF; + + PERFORM weighted_downvotes_after_act(item_id, user_id, act_sats); + END IF; + + INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) + VALUES (act_msats, item_id, user_id, act, now_utc(), now_utc()) + RETURNING id INTO item_act_id; + END IF; + + -- they have a referrer and the referrer isn't the one tipping them + IF referrer_id IS NOT NULL AND user_id <> referrer_id THEN + PERFORM referral_act(referrer_id, item_act_id); + END IF; + + RETURN 0; +END; +$$; + +CREATE OR REPLACE FUNCTION run_auction(item_id INTEGER) RETURNS void AS $$ + DECLARE + bid_sats INTEGER; + user_msats BIGINT; + user_id INTEGER; + item_status "Status"; + status_updated_at timestamp(3); + BEGIN + PERFORM ASSERT_SERIALIZED(); + + -- extract data we need + SELECT "maxBid", "userId", status, "statusUpdatedAt" INTO bid_sats, user_id, item_status, status_updated_at FROM "Item" WHERE id = item_id; + SELECT msats INTO user_msats FROM users WHERE id = user_id; + + -- 0 bid items expire after 30 days unless updated + IF bid_sats = 0 THEN + IF item_status <> 'STOPPED' THEN + IF status_updated_at < now_utc() - INTERVAL '30 days' THEN + UPDATE "Item" SET status = 'STOPPED', "statusUpdatedAt" = now_utc() WHERE id = item_id; + ELSEIF item_status = 'NOSATS' THEN + UPDATE "Item" SET status = 'ACTIVE' WHERE id = item_id; + END IF; + END IF; + RETURN; + END IF; + + -- check if user wallet has enough sats + IF bid_sats * 1000 > user_msats THEN + -- if not, set status = NOSATS and statusUpdatedAt to now_utc if not already set + IF item_status <> 'NOSATS' THEN + UPDATE "Item" SET status = 'NOSATS', "statusUpdatedAt" = now_utc() WHERE id = item_id; + END IF; + ELSE + PERFORM item_act(item_id, user_id, 'STREAM', bid_sats); + + -- update item status = ACTIVE and statusUpdatedAt = now_utc if NOSATS + IF item_status = 'NOSATS' THEN + UPDATE "Item" SET status = 'ACTIVE', "statusUpdatedAt" = now_utc() WHERE id = item_id; + END IF; + END IF; + END; +$$ LANGUAGE plpgsql; + +-- retro actively, turn all invites into referrals +UPDATE users +SET "referrerId" = subquery.inviter +FROM ( + SELECT invitees.id AS invitee, inviters.id AS inviter + FROM users invitees + JOIN "Invite" ON invitees."inviteId" = "Invite".id + JOIN users inviters ON inviters.id = "Invite"."userId") subquery +WHERE id = subquery.invitee; + +-- make inviters referrers too +CREATE OR REPLACE FUNCTION invite_drain(user_id INTEGER, invite_id TEXT) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + inviter_id INTEGER; + inviter_sats INTEGER; + gift INTEGER; +BEGIN + PERFORM ASSERT_SERIALIZED(); + -- check user was created in last hour + -- check user did not already redeem an invite + PERFORM FROM users + WHERE id = user_id AND users.created_at >= NOW() AT TIME ZONE 'UTC' - INTERVAL '1 HOUR' + AND users."inviteId" IS NULL; + IF NOT FOUND THEN + RAISE EXCEPTION 'SN_INELIGIBLE'; + END IF; + + -- check that invite has not reached limit + -- check that invite is not revoked + SELECT "Invite"."userId", "Invite".gift INTO inviter_id, gift FROM "Invite" + LEFT JOIN users ON users."inviteId" = invite_id + WHERE "Invite".id = invite_id AND NOT "Invite".revoked + GROUP BY "Invite".id + HAVING COUNT(DISTINCT users.id) < "Invite".limit OR "Invite".limit IS NULL; + IF NOT FOUND THEN + RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED'; + END IF; + + -- check that inviter has sufficient balance + SELECT (msats / 1000) INTO inviter_sats + FROM users WHERE id = inviter_id; + IF inviter_sats < gift THEN + RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED'; + END IF; + + -- subtract amount from inviter + UPDATE users SET msats = msats - (1000 * gift) WHERE id = inviter_id; + -- add amount to invitee + UPDATE users SET msats = msats + (1000 * gift), "inviteId" = invite_id, "referrerId" = inviter_id WHERE id = user_id; + + RETURN 0; +END; +$$; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 185caf3c..814d1138 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,6 +47,11 @@ model User { upvotePopover Boolean @default(false) tipPopover Boolean @default(false) + // referrals + referrer User? @relation("referrals", fields: [referrerId], references: [id]) + referrerId Int? + referrees User[] @relation("referrals") + // tip settings tipDefault Int @default(10) turboTipping Boolean @default(false) @@ -68,10 +73,11 @@ model User { wildWestMode Boolean @default(false) greeterMode Boolean @default(false) - Earn Earn[] - Upload Upload[] @relation(name: "Uploads") - PollVote PollVote[] - Donation Donation[] + Earn Earn[] + Upload Upload[] @relation(name: "Uploads") + PollVote PollVote[] + Donation Donation[] + ReferralAct ReferralAct[] @@index([createdAt]) @@index([inviteId]) @@ -318,6 +324,17 @@ model Pin { Item Item[] } +model ReferralAct { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at") + referrerId Int + referrer User @relation(fields: [referrerId], references: [id]) + itemActId Int + itemAct ItemAct @relation(fields: [itemActId], references: [id]) + msats BigInt +} + enum ItemActType { VOTE BOOST @@ -329,15 +346,16 @@ enum ItemActType { } model ItemAct { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @updatedAt @map(name: "updated_at") - msats BigInt - act ItemActType - item Item @relation(fields: [itemId], references: [id]) - itemId Int - user User @relation(fields: [userId], references: [id]) - userId Int + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + msats BigInt + act ItemActType + item Item @relation(fields: [itemId], references: [id]) + itemId Int + user User @relation(fields: [userId], references: [id]) + userId Int + ReferralAct ReferralAct[] @@index([itemId]) @@index([userId]) diff --git a/svgs/share-fill.svg b/svgs/share-fill.svg new file mode 100644 index 00000000..ef3faa93 --- /dev/null +++ b/svgs/share-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svgs/share-forward-fill.svg b/svgs/share-forward-fill.svg new file mode 100644 index 00000000..1c7c3f4d --- /dev/null +++ b/svgs/share-forward-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/worker/earn.js b/worker/earn.js index 4b07a2cb..5adde9b4 100644 --- a/worker/earn.js +++ b/worker/earn.js @@ -11,9 +11,10 @@ function earn ({ models }) { // compute how much sn earned today let [{ sum }] = await models.$queryRaw` - SELECT coalesce(sum("ItemAct".msats), 0) as sum + SELECT coalesce(sum("ItemAct".msats - coalesce("ReferralAct".msats, 0)), 0) as sum FROM "ItemAct" - JOIN "Item" on "ItemAct"."itemId" = "Item".id + JOIN "Item" ON "ItemAct"."itemId" = "Item".id + LEFT JOIN "ReferralAct" ON "ItemAct".id = "ReferralAct"."itemActId" WHERE "ItemAct".act <> 'TIP' AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'` From ee17518abfca9e168b1ab27012f879d5963957eb Mon Sep 17 00:00:00 2001 From: keyan Date: Mon, 19 Dec 2022 17:00:53 -0600 Subject: [PATCH 09/11] add referrer stats to top --- api/resolvers/user.js | 39 ++++++++++++++++++++++++++++++++++++--- api/typeDefs/user.js | 1 + components/top-header.js | 2 +- components/user-list.js | 1 + fragments/users.js | 1 + 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 8bb44ac7..7794b84c 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -135,6 +135,18 @@ export default { ORDER BY ncomments DESC NULLS LAST, users.created_at DESC OFFSET $2 LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) + } else if (sort === 'referrals') { + users = await models.$queryRaw(` + SELECT users.*, count(*) as referrals + FROM users + JOIN "users" referree on users.id = referree."referrerId" + WHERE referree.created_at <= $1 + AND NOT users."hideFromTopUsers" + ${within('referree', when)} + GROUP BY users.id + ORDER BY referrals DESC NULLS LAST, users.created_at DESC + OFFSET $2 + LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) } else { users = await models.$queryRaw(` SELECT u.id, u.name, u."photoId", floor(sum(amount)/1000) as stacked @@ -151,7 +163,13 @@ export default { FROM "Earn" JOIN users on users.id = "Earn"."userId" WHERE "Earn".msats > 0 ${within('Earn', when)} - AND NOT users."hideFromTopUsers")) u + AND NOT users."hideFromTopUsers") + UNION ALL + (SELECT users.*, "ReferralAct".msats as amount + FROM "ReferralAct" + JOIN users on users.id = "ReferralAct"."referrerId" + WHERE "ReferralAct".msats > 0 ${within('ReferralAct', when)} + AND NOT users."hideFromTopUsers")) u GROUP BY u.id, u.name, u.created_at, u."photoId" ORDER BY stacked DESC NULLS LAST, created_at DESC OFFSET $2 @@ -453,13 +471,18 @@ export default { const [{ stacked }] = await models.$queryRaw(` SELECT sum(amount) as stacked FROM - ((SELECT sum("ItemAct".msats) as amount + ((SELECT coalesce(sum("ItemAct".msats),0) as amount FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id WHERE act <> 'BOOST' AND "ItemAct"."userId" <> $2 AND "Item"."userId" = $2 AND "ItemAct".created_at >= $1) UNION ALL - (SELECT sum("Earn".msats) as amount + (SELECT coalesce(sum("ReferralAct".msats),0) as amount + FROM "ReferralAct" + WHERE "ReferralAct".msats > 0 AND "ReferralAct"."referrerId" = $2 + AND "ReferralAct".created_at >= $1) + UNION ALL + (SELECT coalesce(sum("Earn".msats), 0) as amount FROM "Earn" WHERE "Earn".msats > 0 AND "Earn"."userId" = $2 AND "Earn".created_at >= $1)) u`, withinDate(when), Number(user.id)) @@ -485,6 +508,16 @@ export default { return (msats && msatsToSats(msats)) || 0 }, + referrals: async (user, { when }, { models }) => { + return await models.user.count({ + where: { + referrerId: user.id, + createdAt: { + gte: withinDate(when) + } + } + }) + }, sats: async (user, args, { models, me }) => { if (me?.id !== user.id) { return 0 diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index e79a4722..c6810bd5 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -45,6 +45,7 @@ export default gql` ncomments(when: String): Int! stacked(when: String): Int! spent(when: String): Int! + referrals(when: String): Int! freePosts: Int! freeComments: Int! hasInvites: Boolean! diff --git a/components/top-header.js b/components/top-header.js index 72175c78..e2cffb4f 100644 --- a/components/top-header.js +++ b/components/top-header.js @@ -41,7 +41,7 @@ export default function TopHeader ({ cat }) { onChange={(formik, e) => top({ ...formik?.values, sort: e.target.value })} name='sort' size='sm' - items={cat === 'users' ? ['stacked', 'spent', 'comments', 'posts'] : ['votes', 'comments', 'sats']} + items={cat === 'users' ? ['stacked', 'spent', 'comments', 'posts', 'referrals'] : ['votes', 'comments', 'sats']} /> for