Merge branch 'stackernews:master' into master
This commit is contained in:
		
						commit
						335125f57b
					
				@ -1,6 +1,6 @@
 | 
			
		||||
const PLACEHOLDERS_NUM = 616
 | 
			
		||||
 | 
			
		||||
function interval (when) {
 | 
			
		||||
export function interval (when) {
 | 
			
		||||
  switch (when) {
 | 
			
		||||
    case 'week':
 | 
			
		||||
      return '1 week'
 | 
			
		||||
@ -15,7 +15,7 @@ function interval (when) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function timeUnit (when) {
 | 
			
		||||
export function timeUnit (when) {
 | 
			
		||||
  switch (when) {
 | 
			
		||||
    case 'week':
 | 
			
		||||
    case 'month':
 | 
			
		||||
@ -28,7 +28,7 @@ function timeUnit (when) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function withClause (when) {
 | 
			
		||||
export function withClause (when) {
 | 
			
		||||
  const ival = interval(when)
 | 
			
		||||
  const unit = timeUnit(when)
 | 
			
		||||
 | 
			
		||||
@ -44,7 +44,7 @@ function withClause (when) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HACKY AF this is a performance enhancement that allows us to use the created_at indices on tables
 | 
			
		||||
function intervalClause (when, table, and) {
 | 
			
		||||
export function intervalClause (when, table, and) {
 | 
			
		||||
  if (when === 'forever') {
 | 
			
		||||
    return and ? '' : 'TRUE'
 | 
			
		||||
  }
 | 
			
		||||
@ -58,7 +58,7 @@ export default {
 | 
			
		||||
      return await models.$queryRaw(
 | 
			
		||||
        `${withClause(when)}
 | 
			
		||||
        SELECT time, json_build_array(
 | 
			
		||||
          json_build_object('name', 'invited', 'value', count("inviteId")),
 | 
			
		||||
          json_build_object('name', 'referrals', 'value', count("referrerId")),
 | 
			
		||||
          json_build_object('name', 'organic', 'value', count(users.id) FILTER(WHERE id > ${PLACEHOLDERS_NUM}) - count("inviteId"))
 | 
			
		||||
        ) AS data
 | 
			
		||||
        FROM times
 | 
			
		||||
@ -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`)
 | 
			
		||||
    },
 | 
			
		||||
@ -116,7 +131,8 @@ export default {
 | 
			
		||||
          json_build_object('name', 'any', 'value', count(distinct user_id)),
 | 
			
		||||
          json_build_object('name', 'posts', 'value', count(distinct user_id) FILTER (WHERE type = 'POST')),
 | 
			
		||||
          json_build_object('name', 'comments', 'value', count(distinct user_id) FILTER (WHERE type = 'COMMENT')),
 | 
			
		||||
          json_build_object('name', 'rewards', 'value', count(distinct user_id) FILTER (WHERE type = 'EARN'))
 | 
			
		||||
          json_build_object('name', 'rewards', 'value', count(distinct user_id) FILTER (WHERE type = 'EARN')),
 | 
			
		||||
          json_build_object('name', 'referrals', 'value', count(distinct user_id) FILTER (WHERE type = 'REFERRAL'))
 | 
			
		||||
        ) AS data
 | 
			
		||||
        FROM times
 | 
			
		||||
        LEFT JOIN
 | 
			
		||||
@ -127,7 +143,11 @@ export default {
 | 
			
		||||
        UNION ALL
 | 
			
		||||
        (SELECT created_at, "userId" as user_id, 'EARN' as type
 | 
			
		||||
          FROM "Earn"
 | 
			
		||||
          WHERE ${intervalClause(when, 'Earn', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
 | 
			
		||||
          WHERE ${intervalClause(when, 'Earn', false)})
 | 
			
		||||
        UNION ALL
 | 
			
		||||
          (SELECT created_at, "referrerId" as user_id, 'REFERRAL' as type
 | 
			
		||||
            FROM "ReferralAct"
 | 
			
		||||
            WHERE ${intervalClause(when, 'ReferralAct', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
 | 
			
		||||
        GROUP BY time
 | 
			
		||||
        ORDER BY time ASC`)
 | 
			
		||||
    },
 | 
			
		||||
@ -137,18 +157,24 @@ export default {
 | 
			
		||||
        SELECT time, json_build_array(
 | 
			
		||||
          json_build_object('name', 'rewards', 'value', coalesce(floor(sum(airdrop)/1000),0)),
 | 
			
		||||
          json_build_object('name', 'posts', 'value', coalesce(floor(sum(post)/1000),0)),
 | 
			
		||||
          json_build_object('name', 'comments', 'value', coalesce(floor(sum(comment)/1000),0))
 | 
			
		||||
          json_build_object('name', 'comments', 'value', coalesce(floor(sum(comment)/1000),0)),
 | 
			
		||||
          json_build_object('name', 'referrals', 'value', coalesce(floor(sum(referral)/1000),0))
 | 
			
		||||
        ) AS data
 | 
			
		||||
        FROM times
 | 
			
		||||
        LEFT JOIN
 | 
			
		||||
        ((SELECT "ItemAct".created_at, 0 as airdrop,
 | 
			
		||||
          CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment,
 | 
			
		||||
          CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post
 | 
			
		||||
          CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post,
 | 
			
		||||
          0 as referral
 | 
			
		||||
          FROM "ItemAct"
 | 
			
		||||
          JOIN "Item" on "ItemAct"."itemId" = "Item".id
 | 
			
		||||
          WHERE ${intervalClause(when, 'ItemAct', true)} "ItemAct".act = 'TIP')
 | 
			
		||||
        UNION ALL
 | 
			
		||||
        (SELECT created_at, msats as airdrop, 0 as post, 0 as comment
 | 
			
		||||
          (SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, msats as referral
 | 
			
		||||
            FROM "ReferralAct"
 | 
			
		||||
            WHERE ${intervalClause(when, 'ReferralAct', false)})
 | 
			
		||||
        UNION ALL
 | 
			
		||||
        (SELECT created_at, msats as airdrop, 0 as post, 0 as comment, 0 as referral
 | 
			
		||||
          FROM "Earn"
 | 
			
		||||
          WHERE ${intervalClause(when, 'Earn', false)})) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
 | 
			
		||||
        GROUP BY time
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,9 @@ import sub from './sub'
 | 
			
		||||
import upload from './upload'
 | 
			
		||||
import growth from './growth'
 | 
			
		||||
import search from './search'
 | 
			
		||||
import rewards from './rewards'
 | 
			
		||||
import referrals from './referrals'
 | 
			
		||||
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, referrals, { JSONObject: GraphQLJSONObject }]
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -160,6 +160,15 @@ export default {
 | 
			
		||||
              ORDER BY "sortTime" DESC
 | 
			
		||||
              LIMIT ${LIMIT}+$3)`
 | 
			
		||||
          )
 | 
			
		||||
          queries.push(
 | 
			
		||||
            `(SELECT users.id::text, users.created_at AS "sortTime", NULL as "earnedSats",
 | 
			
		||||
              'Referral' AS type
 | 
			
		||||
              FROM users
 | 
			
		||||
              WHERE "users"."referrerId" = $1
 | 
			
		||||
              AND "inviteId" IS NULL
 | 
			
		||||
              AND users.created_at <= $2
 | 
			
		||||
              LIMIT ${LIMIT}+$3)`
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (meFull.noteEarning) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										55
									
								
								api/resolvers/referrals.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								api/resolvers/referrals.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
import { AuthenticationError } from 'apollo-server-micro'
 | 
			
		||||
import { withClause, intervalClause, timeUnit } from './growth'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  Query: {
 | 
			
		||||
    referrals: async (parent, { when }, { models, me }) => {
 | 
			
		||||
      if (!me) {
 | 
			
		||||
        throw new AuthenticationError('you must be logged in')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const [{ totalSats }] = await models.$queryRaw(`
 | 
			
		||||
        SELECT COALESCE(FLOOR(sum(msats) / 1000), 0) as "totalSats"
 | 
			
		||||
        FROM "ReferralAct"
 | 
			
		||||
        WHERE ${intervalClause(when, 'ReferralAct', true)}
 | 
			
		||||
        "ReferralAct"."referrerId" = $1
 | 
			
		||||
      `, Number(me.id))
 | 
			
		||||
 | 
			
		||||
      const [{ totalReferrals }] = await models.$queryRaw(`
 | 
			
		||||
        SELECT count(*) as "totalReferrals"
 | 
			
		||||
        FROM users
 | 
			
		||||
        WHERE ${intervalClause(when, 'users', true)}
 | 
			
		||||
        "referrerId" = $1
 | 
			
		||||
    `, Number(me.id))
 | 
			
		||||
 | 
			
		||||
      const stats = await models.$queryRaw(
 | 
			
		||||
        `${withClause(when)}
 | 
			
		||||
        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)))
 | 
			
		||||
        ) 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(when, 'ReferralAct', true)}
 | 
			
		||||
            "ReferralAct"."referrerId" = $1)
 | 
			
		||||
        UNION ALL
 | 
			
		||||
        (SELECT created_at, 0.0 as sats, 'REFERREE' as act
 | 
			
		||||
          FROM users
 | 
			
		||||
          WHERE ${intervalClause(when, 'users', true)}
 | 
			
		||||
            "referrerId" = $1)) u ON time = date_trunc('${timeUnit(when)}', u.created_at)
 | 
			
		||||
        GROUP BY time
 | 
			
		||||
        ORDER BY time ASC`, Number(me.id))
 | 
			
		||||
 | 
			
		||||
      console.log(totalSats)
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        totalSats,
 | 
			
		||||
        totalReferrals,
 | 
			
		||||
        stats
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								api/resolvers/rewards.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								api/resolvers/rewards.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
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(FLOOR(sum(sats)), 0) as total, json_build_array(
 | 
			
		||||
          json_build_object('name', 'donations', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'DONATION')), 0)),
 | 
			
		||||
          json_build_object('name', 'fees', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM', 'DONATION'))), 0)),
 | 
			
		||||
          json_build_object('name', 'boost', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'BOOST')), 0)),
 | 
			
		||||
          json_build_object('name', 'jobs', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'STREAM')), 0))
 | 
			
		||||
        ) AS sources
 | 
			
		||||
        FROM (
 | 
			
		||||
          (SELECT ("ItemAct".msats - COALESCE("ReferralAct".msats, 0)) / 1000.0 as sats, act::text as type
 | 
			
		||||
            FROM "ItemAct"
 | 
			
		||||
            LEFT JOIN "ReferralAct" ON "ItemAct".id = "ReferralAct"."itemActId"
 | 
			
		||||
            WHERE "ItemAct".created_at > ${lastReward.createdAt} AND "ItemAct".act <> 'TIP')
 | 
			
		||||
            UNION ALL
 | 
			
		||||
          (SELECT sats::FLOAT, '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
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
@ -127,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
 | 
			
		||||
@ -143,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
 | 
			
		||||
@ -263,6 +289,18 @@ export default {
 | 
			
		||||
        if (newInvitees.length > 0) {
 | 
			
		||||
          return true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const referral = await models.user.findFirst({
 | 
			
		||||
          where: {
 | 
			
		||||
            referrerId: me.id,
 | 
			
		||||
            createdAt: {
 | 
			
		||||
              gt: lastChecked
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        if (referral) {
 | 
			
		||||
          return true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return false
 | 
			
		||||
@ -433,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))
 | 
			
		||||
@ -465,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
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
 | 
			
		||||
import lnpr from 'bolt11'
 | 
			
		||||
import { SELECT } from './item'
 | 
			
		||||
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
 | 
			
		||||
import { msatsToSats } from '../../lib/format'
 | 
			
		||||
import { msatsToSats, msatsToSatsDecimal } from '../../lib/format'
 | 
			
		||||
 | 
			
		||||
export async function getInvoice (parent, { id }, { me, models }) {
 | 
			
		||||
  if (!me) {
 | 
			
		||||
@ -110,6 +110,12 @@ export default {
 | 
			
		||||
            FROM "Earn"
 | 
			
		||||
            WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2
 | 
			
		||||
            GROUP BY "userId", created_at)`)
 | 
			
		||||
        queries.push(
 | 
			
		||||
            `(SELECT ('referral' || "ReferralAct".id) as id, "ReferralAct".id as "factId", NULL as bolt11,
 | 
			
		||||
            created_at as "createdAt", msats,
 | 
			
		||||
            0 as "msatsFee", NULL as status, 'referral' as type
 | 
			
		||||
            FROM "ReferralAct"
 | 
			
		||||
            WHERE "ReferralAct"."referrerId" = $1 AND "ReferralAct".created_at <= $2)`)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (include.has('spent')) {
 | 
			
		||||
@ -122,6 +128,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 +170,9 @@ export default {
 | 
			
		||||
          case 'spent':
 | 
			
		||||
            f.msats *= -1
 | 
			
		||||
            break
 | 
			
		||||
          case 'donation':
 | 
			
		||||
            f.msats *= -1
 | 
			
		||||
            break
 | 
			
		||||
          default:
 | 
			
		||||
            break
 | 
			
		||||
        }
 | 
			
		||||
@ -277,8 +293,8 @@ export default {
 | 
			
		||||
 | 
			
		||||
      return item
 | 
			
		||||
    },
 | 
			
		||||
    sats: fact => msatsToSats(fact.msats),
 | 
			
		||||
    satsFee: fact => msatsToSats(fact.msatsFee)
 | 
			
		||||
    sats: fact => msatsToSatsDecimal(fact.msats),
 | 
			
		||||
    satsFee: fact => msatsToSatsDecimal(fact.msatsFee)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,8 @@ import invite from './invite'
 | 
			
		||||
import sub from './sub'
 | 
			
		||||
import upload from './upload'
 | 
			
		||||
import growth from './growth'
 | 
			
		||||
import rewards from './rewards'
 | 
			
		||||
import referrals from './referrals'
 | 
			
		||||
 | 
			
		||||
const link = gql`
 | 
			
		||||
  type Query {
 | 
			
		||||
@ -26,4 +28,4 @@ const link = gql`
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
export default [link, user, item, message, wallet, lnurl, notifications, invite,
 | 
			
		||||
  sub, upload, growth]
 | 
			
		||||
  sub, upload, growth, rewards, referrals]
 | 
			
		||||
 | 
			
		||||
@ -50,8 +50,12 @@ export default gql`
 | 
			
		||||
    sortTime: String!
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  type Referral {
 | 
			
		||||
    sortTime: String!
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  union Notification = Reply | Votification | Mention
 | 
			
		||||
    | Invitification | Earn | JobChanged | InvoicePaid
 | 
			
		||||
    | Invitification | Earn | JobChanged | InvoicePaid | Referral
 | 
			
		||||
 | 
			
		||||
  type Notifications {
 | 
			
		||||
    lastChecked: String
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								api/typeDefs/referrals.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								api/typeDefs/referrals.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
import { gql } from 'apollo-server-micro'
 | 
			
		||||
 | 
			
		||||
export default gql`
 | 
			
		||||
  extend type Query {
 | 
			
		||||
    referrals(when: String): Referrals!
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  type Referrals {
 | 
			
		||||
    totalSats: Int!
 | 
			
		||||
    totalReferrals: Int!
 | 
			
		||||
    stats: [TimeData!]!
 | 
			
		||||
  }
 | 
			
		||||
`
 | 
			
		||||
							
								
								
									
										16
									
								
								api/typeDefs/rewards.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/typeDefs/rewards.js
									
									
									
									
									
										Normal file
									
								
							@ -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!]!
 | 
			
		||||
  }
 | 
			
		||||
`
 | 
			
		||||
@ -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!
 | 
			
		||||
@ -45,10 +45,12 @@ 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!
 | 
			
		||||
    tipDefault: Int!
 | 
			
		||||
    turboTipping: Boolean!
 | 
			
		||||
    fiatCurrency: String!
 | 
			
		||||
    bio: Item
 | 
			
		||||
    bioId: Int
 | 
			
		||||
 | 
			
		||||
@ -41,8 +41,8 @@ export default gql`
 | 
			
		||||
    factId: ID!
 | 
			
		||||
    bolt11: String
 | 
			
		||||
    createdAt: String!
 | 
			
		||||
    sats: Int!
 | 
			
		||||
    satsFee: Int
 | 
			
		||||
    sats: Float!
 | 
			
		||||
    satsFee: Float
 | 
			
		||||
    status: String
 | 
			
		||||
    type: String!
 | 
			
		||||
    description: String
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ import DontLikeThis from './dont-link-this'
 | 
			
		||||
import Flag from '../svgs/flag-fill.svg'
 | 
			
		||||
import { Badge } from 'react-bootstrap'
 | 
			
		||||
import { abbrNum } from '../lib/format'
 | 
			
		||||
import Share from './share'
 | 
			
		||||
 | 
			
		||||
function Parent ({ item, rootText }) {
 | 
			
		||||
  const ParentFrag = () => (
 | 
			
		||||
@ -169,6 +170,7 @@ export default function Comment ({
 | 
			
		||||
                    localStorage.setItem(`commentCollapse:${item.id}`, 'yep')
 | 
			
		||||
                  }}
 | 
			
		||||
                />)}
 | 
			
		||||
            {topLevel && <Share item={item} />}
 | 
			
		||||
          </div>
 | 
			
		||||
          {edit
 | 
			
		||||
            ? (
 | 
			
		||||
 | 
			
		||||
@ -151,6 +151,13 @@ export default function Footer ({ noLinks }) {
 | 
			
		||||
                <DarkModeIcon onClick={() => darkMode.toggle()} className='fill-grey theme' />
 | 
			
		||||
                <LnIcon onClick={toggleLightning} width={24} height={24} className='ml-2 fill-grey theme' />
 | 
			
		||||
              </div>}
 | 
			
		||||
            <div className='mb-0' style={{ fontWeight: 500 }}>
 | 
			
		||||
              <Link href='/rewards' passHref>
 | 
			
		||||
                <a className='nav-link p-0 d-inline-flex'>
 | 
			
		||||
                  rewards
 | 
			
		||||
                </a>
 | 
			
		||||
              </Link>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className='mb-0' style={{ fontWeight: 500 }}>
 | 
			
		||||
              <OverlayTrigger trigger='click' placement='top' overlay={AnalyticsPopover} rootClose>
 | 
			
		||||
                <div className='nav-link p-0 d-inline-flex' style={{ cursor: 'pointer' }}>
 | 
			
		||||
 | 
			
		||||
@ -92,13 +92,8 @@ export default function Header ({ sub }) {
 | 
			
		||||
                <NavDropdown.Item eventKey='satistics'>satistics</NavDropdown.Item>
 | 
			
		||||
              </Link>
 | 
			
		||||
              <NavDropdown.Divider />
 | 
			
		||||
              <Link href='/invites' passHref>
 | 
			
		||||
                <NavDropdown.Item eventKey='invites'>invites
 | 
			
		||||
                  {me && !me.hasInvites &&
 | 
			
		||||
                    <div className='p-1 d-inline-block bg-success ml-1'>
 | 
			
		||||
                      <span className='invisible'>{' '}</span>
 | 
			
		||||
                    </div>}
 | 
			
		||||
                </NavDropdown.Item>
 | 
			
		||||
              <Link href='/referrals/month' passHref>
 | 
			
		||||
                <NavDropdown.Item eventKey='referrals'>referrals</NavDropdown.Item>
 | 
			
		||||
              </Link>
 | 
			
		||||
              <NavDropdown.Divider />
 | 
			
		||||
              <div className='d-flex align-items-center'>
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
import { InputGroup, Modal } from 'react-bootstrap'
 | 
			
		||||
import { Button, InputGroup, Modal } from 'react-bootstrap'
 | 
			
		||||
import React, { useState, useCallback, useContext, useRef, useEffect } from 'react'
 | 
			
		||||
import * as Yup from 'yup'
 | 
			
		||||
import { Form, Input, SubmitButton } from './form'
 | 
			
		||||
import { useMe } from './me'
 | 
			
		||||
import UpBolt from '../svgs/bolt.svg'
 | 
			
		||||
 | 
			
		||||
export const ItemActContext = React.createContext({
 | 
			
		||||
  item: null,
 | 
			
		||||
@ -38,6 +39,7 @@ export function ItemActModal () {
 | 
			
		||||
  const { item, setItem } = useItemAct()
 | 
			
		||||
  const inputRef = useRef(null)
 | 
			
		||||
  const me = useMe()
 | 
			
		||||
  const [oValue, setOValue] = useState()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    inputRef.current?.focus()
 | 
			
		||||
@ -73,10 +75,26 @@ export function ItemActModal () {
 | 
			
		||||
            label='amount'
 | 
			
		||||
            name='amount'
 | 
			
		||||
            innerRef={inputRef}
 | 
			
		||||
            overrideValue={oValue}
 | 
			
		||||
            required
 | 
			
		||||
            autoFocus
 | 
			
		||||
            append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
			
		||||
          />
 | 
			
		||||
          <div>
 | 
			
		||||
            {[1, 10, 100, 1000, 10000].map(num =>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='sm'
 | 
			
		||||
                className={`${num > 1 ? 'ml-2' : ''} mb-2`}
 | 
			
		||||
                key={num}
 | 
			
		||||
                onClick={() => { setOValue(num) }}
 | 
			
		||||
              >
 | 
			
		||||
                <UpBolt
 | 
			
		||||
                  className='mr-1'
 | 
			
		||||
                  width={14}
 | 
			
		||||
                  height={14}
 | 
			
		||||
                />{num}
 | 
			
		||||
              </Button>)}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className='d-flex'>
 | 
			
		||||
            <SubmitButton variant='success' className='ml-auto mt-1 px-4' value='TIP'>tip</SubmitButton>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import styles from './item.module.css'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { timeSince } from '../lib/time'
 | 
			
		||||
import EmailIcon from '../svgs/mail-open-line.svg'
 | 
			
		||||
import Share from './share'
 | 
			
		||||
 | 
			
		||||
export default function ItemJob ({ item, toc, rank, children }) {
 | 
			
		||||
  const isEmail = Yup.string().email().isValidSync(item.url)
 | 
			
		||||
@ -73,7 +74,11 @@ export default function ItemJob ({ item, toc, rank, children }) {
 | 
			
		||||
            {item.maxBid > 0 && item.status === 'ACTIVE' && <Badge className={`${styles.newComment} ml-1`}>PROMOTED</Badge>}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {toc && <Toc text={item.text} />}
 | 
			
		||||
        {toc &&
 | 
			
		||||
          <>
 | 
			
		||||
            <Share item={item} />
 | 
			
		||||
            <Toc text={item.text} />
 | 
			
		||||
          </>}
 | 
			
		||||
      </div>
 | 
			
		||||
      {children && (
 | 
			
		||||
        <div className={`${styles.children}`} style={{ marginLeft: 'calc(42px + .8rem)' }}>
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ import { newComments } from '../lib/new-comments'
 | 
			
		||||
import { useMe } from './me'
 | 
			
		||||
import DontLikeThis from './dont-link-this'
 | 
			
		||||
import Flag from '../svgs/flag-fill.svg'
 | 
			
		||||
import Share from './share'
 | 
			
		||||
import { abbrNum } from '../lib/format'
 | 
			
		||||
 | 
			
		||||
export function SearchTitle ({ title }) {
 | 
			
		||||
@ -141,7 +142,11 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
 | 
			
		||||
          </div>
 | 
			
		||||
          {showFwdUser && item.fwdUser && <FwdUser user={item.fwdUser} />}
 | 
			
		||||
        </div>
 | 
			
		||||
        {toc && <Toc text={item.text} />}
 | 
			
		||||
        {toc &&
 | 
			
		||||
          <>
 | 
			
		||||
            <Share item={item} />
 | 
			
		||||
            <Toc text={item.text} />
 | 
			
		||||
          </>}
 | 
			
		||||
      </div>
 | 
			
		||||
      {children && (
 | 
			
		||||
        <div className={styles.children}>
 | 
			
		||||
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <div className={styles.page}>
 | 
			
		||||
      <Layout noContain noFooterLinks {...props}>
 | 
			
		||||
      <Layout noContain noFooterLinks={!footerLinks} {...props}>
 | 
			
		||||
        <div className={styles.content}>
 | 
			
		||||
          {children}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ function Notification ({ n }) {
 | 
			
		||||
    <div
 | 
			
		||||
      className='clickToContext'
 | 
			
		||||
      onClick={e => {
 | 
			
		||||
        if (n.__typename === 'Earn') {
 | 
			
		||||
        if (n.__typename === 'Earn' || n.__typename === 'Referral') {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -88,41 +88,50 @@ function Notification ({ n }) {
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            )
 | 
			
		||||
          : n.__typename === 'InvoicePaid'
 | 
			
		||||
          : n.__typename === 'Referral'
 | 
			
		||||
            ? (
 | 
			
		||||
              <div className='font-weight-bold text-info ml-2 py-1'>
 | 
			
		||||
                <Check className='fill-info mr-1' />{n.earnedSats} sats were deposited in your account
 | 
			
		||||
                <small className='text-muted ml-1'>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
              </div>)
 | 
			
		||||
            : (
 | 
			
		||||
              <>
 | 
			
		||||
                {n.__typename === 'Votification' &&
 | 
			
		||||
                  <small className='font-weight-bold text-success ml-2'>
 | 
			
		||||
                    your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
 | 
			
		||||
                  </small>}
 | 
			
		||||
                {n.__typename === 'Mention' &&
 | 
			
		||||
                  <small className='font-weight-bold text-info ml-2'>
 | 
			
		||||
                    you were mentioned in
 | 
			
		||||
                  </small>}
 | 
			
		||||
                {n.__typename === 'JobChanged' &&
 | 
			
		||||
                  <small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}>
 | 
			
		||||
                    {n.item.status === 'ACTIVE'
 | 
			
		||||
                      ? 'your job is active again'
 | 
			
		||||
                      : (n.item.status === 'NOSATS'
 | 
			
		||||
                          ? 'your job promotion ran out of sats'
 | 
			
		||||
                          : 'your job has been stopped')}
 | 
			
		||||
                  </small>}
 | 
			
		||||
                <div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
 | 
			
		||||
                  {n.item.isJob
 | 
			
		||||
                    ? <ItemJob item={n.item} />
 | 
			
		||||
                    : n.item.title
 | 
			
		||||
                      ? <Item item={n.item} />
 | 
			
		||||
                      : (
 | 
			
		||||
                        <div className='pb-2'>
 | 
			
		||||
                          <Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
 | 
			
		||||
                        </div>)}
 | 
			
		||||
                </div>
 | 
			
		||||
              </>)}
 | 
			
		||||
                <small className='font-weight-bold text-secondary ml-2'>
 | 
			
		||||
                  someone joined via one of your <Link href='/referrals/month' passHref><a className='text-reset'>referral links</a></Link>
 | 
			
		||||
                  <small className='text-muted ml-1'>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
                </small>
 | 
			
		||||
              </>
 | 
			
		||||
              )
 | 
			
		||||
            : n.__typename === 'InvoicePaid'
 | 
			
		||||
              ? (
 | 
			
		||||
                <div className='font-weight-bold text-info ml-2 py-1'>
 | 
			
		||||
                  <Check className='fill-info mr-1' />{n.earnedSats} sats were deposited in your account
 | 
			
		||||
                  <small className='text-muted ml-1'>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
                </div>)
 | 
			
		||||
              : (
 | 
			
		||||
                <>
 | 
			
		||||
                  {n.__typename === 'Votification' &&
 | 
			
		||||
                    <small className='font-weight-bold text-success ml-2'>
 | 
			
		||||
                      your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
 | 
			
		||||
                    </small>}
 | 
			
		||||
                  {n.__typename === 'Mention' &&
 | 
			
		||||
                    <small className='font-weight-bold text-info ml-2'>
 | 
			
		||||
                      you were mentioned in
 | 
			
		||||
                    </small>}
 | 
			
		||||
                  {n.__typename === 'JobChanged' &&
 | 
			
		||||
                    <small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}>
 | 
			
		||||
                      {n.item.status === 'ACTIVE'
 | 
			
		||||
                        ? 'your job is active again'
 | 
			
		||||
                        : (n.item.status === 'NOSATS'
 | 
			
		||||
                            ? 'your job promotion ran out of sats'
 | 
			
		||||
                            : 'your job has been stopped')}
 | 
			
		||||
                    </small>}
 | 
			
		||||
                  <div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
 | 
			
		||||
                    {n.item.isJob
 | 
			
		||||
                      ? <ItemJob item={n.item} />
 | 
			
		||||
                      : n.item.title
 | 
			
		||||
                        ? <Item item={n.item} />
 | 
			
		||||
                        : (
 | 
			
		||||
                          <div className='pb-2'>
 | 
			
		||||
                            <Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
 | 
			
		||||
                          </div>)}
 | 
			
		||||
                  </div>
 | 
			
		||||
                </>)}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										46
									
								
								components/share.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								components/share.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
import { Dropdown } from 'react-bootstrap'
 | 
			
		||||
import ShareIcon from '../svgs/share-fill.svg'
 | 
			
		||||
import copy from 'clipboard-copy'
 | 
			
		||||
import { useMe } from './me'
 | 
			
		||||
 | 
			
		||||
export default function Share ({ item }) {
 | 
			
		||||
  const me = useMe()
 | 
			
		||||
  const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
 | 
			
		||||
 | 
			
		||||
  return typeof window !== 'undefined' && navigator?.share
 | 
			
		||||
    ? (
 | 
			
		||||
      <div className='ml-auto pointer d-flex align-items-center'>
 | 
			
		||||
        <ShareIcon
 | 
			
		||||
          className='mx-2 fill-grey theme'
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            if (navigator.share) {
 | 
			
		||||
              navigator.share({
 | 
			
		||||
                title: item.title || '',
 | 
			
		||||
                text: '',
 | 
			
		||||
                url
 | 
			
		||||
              }).then(() => console.log('Successful share'))
 | 
			
		||||
                .catch((error) => console.log('Error sharing', error))
 | 
			
		||||
            } else {
 | 
			
		||||
              console.log('no navigator.share')
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </div>)
 | 
			
		||||
    : (
 | 
			
		||||
      <Dropdown alignRight className='ml-auto pointer  d-flex align-items-center' as='span'>
 | 
			
		||||
        <Dropdown.Toggle variant='success' id='dropdown-basic' as='a'>
 | 
			
		||||
          <ShareIcon className='mx-2 fill-grey theme' />
 | 
			
		||||
        </Dropdown.Toggle>
 | 
			
		||||
 | 
			
		||||
        <Dropdown.Menu>
 | 
			
		||||
          <Dropdown.Item
 | 
			
		||||
            className='text-center'
 | 
			
		||||
            onClick={async () => {
 | 
			
		||||
              copy(url)
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            copy link
 | 
			
		||||
          </Dropdown.Item>
 | 
			
		||||
        </Dropdown.Menu>
 | 
			
		||||
      </Dropdown>)
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
          <Select
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ export default function UserList ({ users }) {
 | 
			
		||||
                {abbrNum(user.ncomments)} comments
 | 
			
		||||
              </a>
 | 
			
		||||
            </Link>
 | 
			
		||||
            {user.referrals > 0 && <span> \ {abbrNum(user.referrals)} referrals</span>}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										161
									
								
								components/when-charts.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								components/when-charts.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,161 @@
 | 
			
		||||
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, AreaChart, Area, ComposedChart, Bar } from 'recharts'
 | 
			
		||||
import { abbrNum } from '../lib/format'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const COLORS = [
 | 
			
		||||
  'var(--secondary)',
 | 
			
		||||
  'var(--info)',
 | 
			
		||||
  'var(--success)',
 | 
			
		||||
  'var(--boost)',
 | 
			
		||||
  'var(--theme-grey)',
 | 
			
		||||
  'var(--danger)'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
export function WhenAreaChart ({ 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 (
 | 
			
		||||
    <ResponsiveContainer width='100%' height={300} minWidth={300}>
 | 
			
		||||
      <AreaChart
 | 
			
		||||
        data={data}
 | 
			
		||||
        margin={{
 | 
			
		||||
          top: 5,
 | 
			
		||||
          right: 5,
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <XAxis
 | 
			
		||||
          dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
 | 
			
		||||
          tick={{ fill: 'var(--theme-grey)' }}
 | 
			
		||||
        />
 | 
			
		||||
        <YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
 | 
			
		||||
        <Legend />
 | 
			
		||||
        {Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
 | 
			
		||||
          <Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
 | 
			
		||||
      </AreaChart>
 | 
			
		||||
    </ResponsiveContainer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function WhenLineChart ({ 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 (
 | 
			
		||||
    <ResponsiveContainer width='100%' height={300} minWidth={300}>
 | 
			
		||||
      <LineChart
 | 
			
		||||
        data={data}
 | 
			
		||||
        margin={{
 | 
			
		||||
          top: 5,
 | 
			
		||||
          right: 5,
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <XAxis
 | 
			
		||||
          dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
 | 
			
		||||
          tick={{ fill: 'var(--theme-grey)' }}
 | 
			
		||||
        />
 | 
			
		||||
        <YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
 | 
			
		||||
        <Legend />
 | 
			
		||||
        {Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
 | 
			
		||||
          <Line key={v} type='monotone' dataKey={v} name={v} stroke={COLORS[i]} fill={COLORS[i]} />)}
 | 
			
		||||
      </LineChart>
 | 
			
		||||
    </ResponsiveContainer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function WhenComposedChart ({ data, lineNames, areaNames, barNames }) {
 | 
			
		||||
  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 (
 | 
			
		||||
    <ResponsiveContainer width='100%' height={300} minWidth={300}>
 | 
			
		||||
      <ComposedChart
 | 
			
		||||
        data={data}
 | 
			
		||||
        margin={{
 | 
			
		||||
          top: 5,
 | 
			
		||||
          right: 5,
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <XAxis
 | 
			
		||||
          dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
 | 
			
		||||
          tick={{ fill: 'var(--theme-grey)' }}
 | 
			
		||||
        />
 | 
			
		||||
        <YAxis yAxisId='left' orientation='left' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <YAxis yAxisId='right' orientation='right' allowDecimals={false} stroke='var(--theme-grey)' tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
 | 
			
		||||
        <Legend />
 | 
			
		||||
        {barNames?.map((v, i) =>
 | 
			
		||||
          <Bar yAxisId='right' key={v} type='monotone' dataKey={v} name={v} stroke='var(--info)' fill='var(--info)' />)}
 | 
			
		||||
        {areaNames?.map((v, i) =>
 | 
			
		||||
          <Area yAxisId='left' key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
 | 
			
		||||
        {lineNames?.map((v, i) =>
 | 
			
		||||
          <Line yAxisId='left' key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
 | 
			
		||||
      </ComposedChart>
 | 
			
		||||
    </ResponsiveContainer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -37,6 +37,9 @@ export const NOTIFICATIONS = gql`
 | 
			
		||||
            tips
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        ... on Referral {
 | 
			
		||||
          sortTime
 | 
			
		||||
        }
 | 
			
		||||
        ... on Reply {
 | 
			
		||||
          sortTime
 | 
			
		||||
          item {
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
@ -133,6 +135,7 @@ export const TOP_USERS = gql`
 | 
			
		||||
        spent(when: $when)
 | 
			
		||||
        ncomments(when: $when)
 | 
			
		||||
        nitems(when: $when)
 | 
			
		||||
        referrals(when: $when)
 | 
			
		||||
      }
 | 
			
		||||
      cursor
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
export const NOFOLLOW_LIMIT = 1000
 | 
			
		||||
export const NOFOLLOW_LIMIT = 100
 | 
			
		||||
export const BOOST_MIN = 5000
 | 
			
		||||
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
 | 
			
		||||
export const IMAGE_PIXELS_MAX = 35000000
 | 
			
		||||
 | 
			
		||||
@ -16,3 +16,10 @@ export const msatsToSats = msats => {
 | 
			
		||||
  }
 | 
			
		||||
  return Number(BigInt(msats) / 1000n)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const msatsToSatsDecimal = msats => {
 | 
			
		||||
  if (msats === null || msats === undefined) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
  return fixedDecimal(msats / 1000.0, 3)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								middleware.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								middleware.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
import { NextResponse } from 'next/server'
 | 
			
		||||
 | 
			
		||||
export function middleware (request) {
 | 
			
		||||
  const regex = /(\/.*)?\/r\/([\w_]+)/
 | 
			
		||||
  const m = regex.exec(request.nextUrl.pathname)
 | 
			
		||||
 | 
			
		||||
  const url = new URL(m[1] || '/', request.url)
 | 
			
		||||
  url.search = request.nextUrl.search
 | 
			
		||||
  url.hash = request.nextUrl.hash
 | 
			
		||||
 | 
			
		||||
  const resp = NextResponse.redirect(url)
 | 
			
		||||
  resp.cookies.set('sn_referrer', m[2])
 | 
			
		||||
  return resp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const config = {
 | 
			
		||||
  matcher: ['/(.*/|)r/([\\w_]+)([?#]?.*)']
 | 
			
		||||
}
 | 
			
		||||
@ -5,9 +5,7 @@ import prisma from '../../../api/models'
 | 
			
		||||
import nodemailer from 'nodemailer'
 | 
			
		||||
import { getSession } from 'next-auth/client'
 | 
			
		||||
 | 
			
		||||
export default (req, res) => NextAuth(req, res, options)
 | 
			
		||||
 | 
			
		||||
const options = {
 | 
			
		||||
export default (req, res) => NextAuth(req, res, {
 | 
			
		||||
  callbacks: {
 | 
			
		||||
    /**
 | 
			
		||||
     * @param  {object}  token     Decrypted JSON Web Token
 | 
			
		||||
@ -26,22 +24,32 @@ const options = {
 | 
			
		||||
        token.user = { id: Number(user.id) }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // sign them up for the newsletter
 | 
			
		||||
      if (isNewUser && profile.email) {
 | 
			
		||||
        fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
 | 
			
		||||
          method: 'POST',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json',
 | 
			
		||||
            Authorization: 'Basic ' + Buffer.from(process.env.LIST_MONK_AUTH).toString('base64')
 | 
			
		||||
          },
 | 
			
		||||
          body: JSON.stringify({
 | 
			
		||||
            email: profile.email,
 | 
			
		||||
            name: 'blank',
 | 
			
		||||
            lists: [2],
 | 
			
		||||
            status: 'enabled',
 | 
			
		||||
            preconfirm_subscriptions: true
 | 
			
		||||
          })
 | 
			
		||||
        }).then(async r => console.log(await r.json())).catch(console.log)
 | 
			
		||||
      if (isNewUser) {
 | 
			
		||||
        // if referrer exists, set on user
 | 
			
		||||
        if (req.cookies.sn_referrer && user?.id) {
 | 
			
		||||
          const referrer = await prisma.user.findUnique({ where: { name: req.cookies.sn_referrer } })
 | 
			
		||||
          if (referrer) {
 | 
			
		||||
            await prisma.user.update({ where: { id: user.id }, data: { referrerId: referrer.id } })
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // sign them up for the newsletter
 | 
			
		||||
        if (profile.email) {
 | 
			
		||||
          fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            headers: {
 | 
			
		||||
              'Content-Type': 'application/json',
 | 
			
		||||
              Authorization: 'Basic ' + Buffer.from(process.env.LIST_MONK_AUTH).toString('base64')
 | 
			
		||||
            },
 | 
			
		||||
            body: JSON.stringify({
 | 
			
		||||
              email: profile.email,
 | 
			
		||||
              name: 'blank',
 | 
			
		||||
              lists: [2],
 | 
			
		||||
              status: 'enabled',
 | 
			
		||||
              preconfirm_subscriptions: true
 | 
			
		||||
            })
 | 
			
		||||
          }).then(async r => console.log(await r.json())).catch(console.log)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return token
 | 
			
		||||
@ -130,7 +138,7 @@ const options = {
 | 
			
		||||
  pages: {
 | 
			
		||||
    signIn: '/login'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function sendVerificationRequest ({
 | 
			
		||||
  identifier: email,
 | 
			
		||||
 | 
			
		||||
@ -120,7 +120,7 @@ export default function Invites () {
 | 
			
		||||
        <h2 className='mt-3 mb-0'>
 | 
			
		||||
          invite links
 | 
			
		||||
        </h2>
 | 
			
		||||
        <small className='d-block text-muted font-weight-bold mx-5'>send these to people you trust somewhat, e.g. group chats or DMs</small>
 | 
			
		||||
        <small className='d-block text-muted font-weight-bold mx-5'>send these to people you trust, e.g. group chats or DMs</small>
 | 
			
		||||
      </div>
 | 
			
		||||
      <InviteForm />
 | 
			
		||||
      {active.length > 0 && <InviteList name='active' invites={active} />}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										75
									
								
								pages/referrals/[when].js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								pages/referrals/[when].js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
			
		||||
import { gql } from 'apollo-server-micro'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { getGetServerSideProps } from '../../api/ssrApollo'
 | 
			
		||||
import { CopyInput, Form, Select } from '../../components/form'
 | 
			
		||||
import LayoutCenter from '../../components/layout-center'
 | 
			
		||||
import { useMe } from '../../components/me'
 | 
			
		||||
import { WhenComposedChart } from '../../components/when-charts'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(
 | 
			
		||||
  gql`
 | 
			
		||||
    query Referrals($when: String!)
 | 
			
		||||
    {
 | 
			
		||||
      referrals(when: $when) {
 | 
			
		||||
        totalSats
 | 
			
		||||
        totalReferrals
 | 
			
		||||
        stats {
 | 
			
		||||
          time
 | 
			
		||||
          data {
 | 
			
		||||
            name
 | 
			
		||||
            value
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }`)
 | 
			
		||||
 | 
			
		||||
export default function Referrals ({ data: { referrals: { totalSats, totalReferrals, stats } } }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const me = useMe()
 | 
			
		||||
  return (
 | 
			
		||||
    <LayoutCenter footerLinks>
 | 
			
		||||
      <Form
 | 
			
		||||
        initial={{
 | 
			
		||||
          when: router.query.when
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <h4 className='font-weight-bold text-muted text-center pt-5 pb-3 d-flex align-items-center justify-content-center'>
 | 
			
		||||
          {totalReferrals} referrals & {totalSats} sats in the last
 | 
			
		||||
          <Select
 | 
			
		||||
            groupClassName='mb-0 ml-2'
 | 
			
		||||
            className='w-auto'
 | 
			
		||||
            name='when'
 | 
			
		||||
            size='sm'
 | 
			
		||||
            items={['day', 'week', 'month', 'year', 'forever']}
 | 
			
		||||
            onChange={(formik, e) => router.push(`/referrals/${e.target.value}`)}
 | 
			
		||||
          />
 | 
			
		||||
        </h4>
 | 
			
		||||
      </Form>
 | 
			
		||||
      <WhenComposedChart data={stats} lineNames={['sats']} barNames={['referrals']} />
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        className='text-small pt-5 px-3 d-flex w-100 align-items-center'
 | 
			
		||||
      >
 | 
			
		||||
        <div className='nav-item text-muted pr-2' style={{ 'white-space': 'nowrap' }}>referral link:</div>
 | 
			
		||||
        <CopyInput
 | 
			
		||||
          size='sm'
 | 
			
		||||
          groupClassName='mb-0 w-100'
 | 
			
		||||
          readOnly
 | 
			
		||||
          noForm
 | 
			
		||||
          placeholder={`https://stacker.news/r/${me.name}`}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <ul className='py-3 text-muted'>
 | 
			
		||||
        <li>{`appending /r/${me.name} to any SN link makes it a ref link`}
 | 
			
		||||
          <ul>
 | 
			
		||||
            <li>e.g. https://stacker.news/items/1/r/{me.name}</li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>earn 21% of boost and job fees spent by referred stackers</li>
 | 
			
		||||
        <li>earn 2.1% of all tips received by referred stackers</li>
 | 
			
		||||
        <li><Link href='/invites' passHref><a>invite links</a></Link> are also implicitly referral links</li>
 | 
			
		||||
      </ul>
 | 
			
		||||
    </LayoutCenter>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										142
									
								
								pages/rewards.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								pages/rewards.js
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <LayoutCenter footerLinks>
 | 
			
		||||
      <h4 className='font-weight-bold text-muted text-center'>
 | 
			
		||||
        <div>{total} sats to be rewarded today</div>
 | 
			
		||||
        <Link href='/faq#how-do-i-earn-sats-on-stacker-news' passHref>
 | 
			
		||||
          <a className='text-reset'><small><small><small>learn about rewards</small></small></small></a>
 | 
			
		||||
        </Link>
 | 
			
		||||
      </h4>
 | 
			
		||||
      <div className='my-3 w-100'>
 | 
			
		||||
        <GrowthPieChart data={sources} />
 | 
			
		||||
      </div>
 | 
			
		||||
      <DonateButton />
 | 
			
		||||
    </LayoutCenter>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const COLORS = [
 | 
			
		||||
  'var(--secondary)',
 | 
			
		||||
  'var(--info)',
 | 
			
		||||
  'var(--success)',
 | 
			
		||||
  'var(--boost)',
 | 
			
		||||
  'var(--grey)'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
function GrowthPieChart ({ data }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ResponsiveContainer width='100%' height={250} minWidth={200}>
 | 
			
		||||
      <PieChart margin={{ top: 5, right: 5, bottom: 5, left: 5 }}>
 | 
			
		||||
        <Pie
 | 
			
		||||
          dataKey='value'
 | 
			
		||||
          isAnimationActive={false}
 | 
			
		||||
          data={data}
 | 
			
		||||
          cx='50%'
 | 
			
		||||
          cy='50%'
 | 
			
		||||
          outerRadius={80}
 | 
			
		||||
          fill='var(--secondary)'
 | 
			
		||||
          label
 | 
			
		||||
        >
 | 
			
		||||
          {
 | 
			
		||||
            data.map((entry, index) => (
 | 
			
		||||
              <Cell key={`cell-${index}`} fill={COLORS[index]} />
 | 
			
		||||
            ))
 | 
			
		||||
          }
 | 
			
		||||
        </Pie>
 | 
			
		||||
        <Tooltip />
 | 
			
		||||
      </PieChart>
 | 
			
		||||
    </ResponsiveContainer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <Button onClick={() => setShow(true)}>DONATE TO REWARDS</Button>
 | 
			
		||||
      <Modal
 | 
			
		||||
        show={show}
 | 
			
		||||
        onHide={() => {
 | 
			
		||||
          setShow(false)
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <div className='modal-close' onClick={() => setShow(false)}>X</div>
 | 
			
		||||
        <Modal.Body>
 | 
			
		||||
          <Form
 | 
			
		||||
            initial={{
 | 
			
		||||
              amount: 1000
 | 
			
		||||
            }}
 | 
			
		||||
            schema={DonateSchema}
 | 
			
		||||
            onSubmit={async ({ amount }) => {
 | 
			
		||||
              await donateToRewards({
 | 
			
		||||
                variables: {
 | 
			
		||||
                  sats: Number(amount)
 | 
			
		||||
                }
 | 
			
		||||
              })
 | 
			
		||||
              setShow(false)
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Input
 | 
			
		||||
              label='amount'
 | 
			
		||||
              name='amount'
 | 
			
		||||
              innerRef={inputRef}
 | 
			
		||||
              required
 | 
			
		||||
              autoFocus
 | 
			
		||||
              append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
			
		||||
            />
 | 
			
		||||
            <div className='d-flex'>
 | 
			
		||||
              <SubmitButton variant='success' className='ml-auto mt-1 px-4' value='TIP'>donate</SubmitButton>
 | 
			
		||||
            </div>
 | 
			
		||||
          </Form>
 | 
			
		||||
        </Modal.Body>
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -98,6 +97,25 @@ function Detail ({ fact }) {
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  if (fact.type === 'donation') {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className={satusClass(fact.status)}>
 | 
			
		||||
          You made a donation to <Link href='/rewards' passHref><a>daily rewards</a></Link>!
 | 
			
		||||
        </div>
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  if (fact.type === 'referral') {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className={satusClass(fact.status)}>
 | 
			
		||||
          You stacked sats from <Link href='/referrals/month' passHref><a>a referral</a></Link>!
 | 
			
		||||
        </div>
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!fact.item) {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
@ -145,6 +163,8 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
 | 
			
		||||
      case 'invoice':
 | 
			
		||||
        return `/${fact.type}s/${fact.factId}`
 | 
			
		||||
      case 'earn':
 | 
			
		||||
      case 'donation':
 | 
			
		||||
      case 'referral':
 | 
			
		||||
        return
 | 
			
		||||
      default:
 | 
			
		||||
        return `/items/${fact.factId}`
 | 
			
		||||
@ -201,11 +221,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
 | 
			
		||||
              <th className={styles.type}>type</th>
 | 
			
		||||
              <th>detail</th>
 | 
			
		||||
              <th className={styles.sats}>
 | 
			
		||||
                <div>sats
 | 
			
		||||
                  <Info>
 | 
			
		||||
                    <div className='font-weight-bold'>Sats are rounded down from millisats to the nearest sat, so the actual amount might be slightly larger.</div>
 | 
			
		||||
                  </Info>
 | 
			
		||||
                </div>
 | 
			
		||||
                sats
 | 
			
		||||
              </th>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </thead>
 | 
			
		||||
@ -220,7 +236,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
 | 
			
		||||
                    <td className={styles.description}>
 | 
			
		||||
                      <Detail fact={f} />
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td className={`${styles.sats} ${satusClass(f.status)}`}>{Math.floor(f.sats)}</td>
 | 
			
		||||
                    <td className={`${styles.sats} ${satusClass(f.status)}`}>{f.sats}</td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </Wrapper>
 | 
			
		||||
              )
 | 
			
		||||
 | 
			
		||||
@ -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 } }) {
 | 
			
		||||
        <Form
 | 
			
		||||
          initial={{
 | 
			
		||||
            tipDefault: settings?.tipDefault || 21,
 | 
			
		||||
            turboTipping: settings?.turboTipping,
 | 
			
		||||
            fiatCurrency: settings?.fiatCurrency || 'USD',
 | 
			
		||||
            noteItemSats: settings?.noteItemSats,
 | 
			
		||||
            noteEarning: settings?.noteEarning,
 | 
			
		||||
@ -82,10 +84,44 @@ export default function Settings ({ data: { settings } }) {
 | 
			
		||||
          <Input
 | 
			
		||||
            label='tip default'
 | 
			
		||||
            name='tipDefault'
 | 
			
		||||
            groupClassName='mb-0'
 | 
			
		||||
            required
 | 
			
		||||
            autoFocus
 | 
			
		||||
            append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
			
		||||
            hint={<small className='text-muted'>note: you can also press and hold the lightning bolt to tip custom amounts</small>}
 | 
			
		||||
          />
 | 
			
		||||
          <div className='mb-2'>
 | 
			
		||||
            <AccordianItem
 | 
			
		||||
              show={settings?.turboTipping}
 | 
			
		||||
              header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>}
 | 
			
		||||
              body={<Checkbox
 | 
			
		||||
                name='turboTipping'
 | 
			
		||||
                label={
 | 
			
		||||
                  <div className='d-flex align-items-center'>turbo tipping
 | 
			
		||||
                    <Info>
 | 
			
		||||
                      <ul className='font-weight-bold'>
 | 
			
		||||
                        <li>Makes every additional bolt click raise your total tip to another 10x multiple of your default tip</li>
 | 
			
		||||
                        <li>e.g. if your tip default is 10 sats
 | 
			
		||||
                          <ul>
 | 
			
		||||
                            <li>1st click: 10 sats total tipped</li>
 | 
			
		||||
                            <li>2nd click: 100 sats total tipped</li>
 | 
			
		||||
                            <li>3rd click: 1000 sats total tipped</li>
 | 
			
		||||
                            <li>4th click: 10000 sats total tipped</li>
 | 
			
		||||
                            <li>and so on ...</li>
 | 
			
		||||
                          </ul>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li>You can still custom tip via long press
 | 
			
		||||
                          <ul>
 | 
			
		||||
                            <li>the next bolt click rounds up to the next greatest 10x multiple of your default</li>
 | 
			
		||||
                          </ul>
 | 
			
		||||
                        </li>
 | 
			
		||||
                      </ul>
 | 
			
		||||
                    </Info>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  }
 | 
			
		||||
                    />}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <Select
 | 
			
		||||
            label='fiat currency'
 | 
			
		||||
            name='fiatCurrency'
 | 
			
		||||
@ -110,7 +146,7 @@ export default function Settings ({ data: { settings } }) {
 | 
			
		||||
            groupClassName='mb-0'
 | 
			
		||||
          />
 | 
			
		||||
          <Checkbox
 | 
			
		||||
            label='my invite links are redeemed'
 | 
			
		||||
            label='someone joins using my invite or referral links'
 | 
			
		||||
            name='noteInvites'
 | 
			
		||||
            groupClassName='mb-0'
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
@ -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,111 +62,33 @@ export default function Growth ({
 | 
			
		||||
      <Row>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>stackers</div>
 | 
			
		||||
          <GrowthLineChart data={stackerGrowth} />
 | 
			
		||||
          <WhenLineChart data={stackerGrowth} />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>stacking</div>
 | 
			
		||||
          <GrowthAreaChart data={stackingGrowth} />
 | 
			
		||||
          <WhenAreaChart data={stackingGrowth} />
 | 
			
		||||
        </Col>
 | 
			
		||||
      </Row>
 | 
			
		||||
      <Row>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>spenders</div>
 | 
			
		||||
          <GrowthLineChart data={spenderGrowth} />
 | 
			
		||||
          <WhenLineChart data={spenderGrowth} />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>spending</div>
 | 
			
		||||
          <GrowthAreaChart data={spendingGrowth} />
 | 
			
		||||
          <WhenAreaChart data={spendingGrowth} />
 | 
			
		||||
        </Col>
 | 
			
		||||
      </Row>
 | 
			
		||||
      <Row>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>registrations</div>
 | 
			
		||||
          <GrowthAreaChart data={registrationGrowth} />
 | 
			
		||||
          <WhenAreaChart data={registrationGrowth} />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>items</div>
 | 
			
		||||
          <GrowthAreaChart data={itemGrowth} />
 | 
			
		||||
          <WhenAreaChart data={itemGrowth} />
 | 
			
		||||
        </Col>
 | 
			
		||||
      </Row>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const COLORS = [
 | 
			
		||||
  'var(--secondary)',
 | 
			
		||||
  'var(--info)',
 | 
			
		||||
  'var(--success)',
 | 
			
		||||
  'var(--boost)',
 | 
			
		||||
  'var(--theme-grey)'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
    <ResponsiveContainer width='100%' height={300} minWidth={300}>
 | 
			
		||||
      <AreaChart
 | 
			
		||||
        data={data}
 | 
			
		||||
        margin={{
 | 
			
		||||
          top: 5,
 | 
			
		||||
          right: 5,
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <XAxis
 | 
			
		||||
          dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
 | 
			
		||||
          tick={{ fill: 'var(--theme-grey)' }}
 | 
			
		||||
        />
 | 
			
		||||
        <YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
 | 
			
		||||
        <Legend />
 | 
			
		||||
        {Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
 | 
			
		||||
          <Area key={v} type='monotone' dataKey={v} name={v} stackId='1' stroke={COLORS[i]} fill={COLORS[i]} />)}
 | 
			
		||||
      </AreaChart>
 | 
			
		||||
    </ResponsiveContainer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
    <ResponsiveContainer width='100%' height={300} minWidth={300}>
 | 
			
		||||
      <LineChart
 | 
			
		||||
        data={data}
 | 
			
		||||
        margin={{
 | 
			
		||||
          top: 5,
 | 
			
		||||
          right: 5,
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <XAxis
 | 
			
		||||
          dataKey='time' tickFormatter={dateFormatter(when)} name={xAxisName(when)}
 | 
			
		||||
          tick={{ fill: 'var(--theme-grey)' }}
 | 
			
		||||
        />
 | 
			
		||||
        <YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
 | 
			
		||||
        <Legend />
 | 
			
		||||
        {Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
 | 
			
		||||
          <Line key={v} type='monotone' dataKey={v} name={v} stroke={COLORS[i]} fill={COLORS[i]} />)}
 | 
			
		||||
      </LineChart>
 | 
			
		||||
    </ResponsiveContainer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								prisma/migrations/20221206213226_donate/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								prisma/migrations/20221206213226_donate/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
$$;
 | 
			
		||||
							
								
								
									
										25
									
								
								prisma/migrations/20221207212053_donate_func/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								prisma/migrations/20221207212053_donate_func/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
$$;
 | 
			
		||||
@ -0,0 +1,2 @@
 | 
			
		||||
-- AlterTable
 | 
			
		||||
ALTER TABLE "users" ADD COLUMN     "turboTipping" BOOLEAN NOT NULL DEFAULT false;
 | 
			
		||||
							
								
								
									
										25
									
								
								prisma/migrations/20221209201457_donate_func2/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								prisma/migrations/20221209201457_donate_func2/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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 "Donation" (sats, "userId", created_at, updated_at)
 | 
			
		||||
    VALUES (sats, user_id, now_utc(), now_utc());
 | 
			
		||||
 | 
			
		||||
    RETURN sats;
 | 
			
		||||
END;
 | 
			
		||||
$$;
 | 
			
		||||
							
								
								
									
										5
									
								
								prisma/migrations/20221213220945_referral/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								prisma/migrations/20221213220945_referral/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
							
								
								
									
										17
									
								
								prisma/migrations/20221214175911_referral_act/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								prisma/migrations/20221214175911_referral_act/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
							
								
								
									
										202
									
								
								prisma/migrations/20221214201527_referral_funcs/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								prisma/migrations/20221214201527_referral_funcs/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
$$;
 | 
			
		||||
@ -35,7 +35,6 @@ model User {
 | 
			
		||||
  freeComments    Int         @default(5)
 | 
			
		||||
  freePosts       Int         @default(2)
 | 
			
		||||
  checkedNotesAt  DateTime?
 | 
			
		||||
  tipDefault      Int         @default(10)
 | 
			
		||||
  fiatCurrency    String      @default("USD")
 | 
			
		||||
  pubkey          String?     @unique
 | 
			
		||||
  trust           Float       @default(0)
 | 
			
		||||
@ -48,6 +47,15 @@ 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)
 | 
			
		||||
 | 
			
		||||
  // notification settings
 | 
			
		||||
  noteItemSats       Boolean @default(true)
 | 
			
		||||
  noteEarning        Boolean @default(true)
 | 
			
		||||
@ -65,15 +73,26 @@ model User {
 | 
			
		||||
  wildWestMode Boolean @default(false)
 | 
			
		||||
  greeterMode  Boolean @default(false)
 | 
			
		||||
 | 
			
		||||
  Earn     Earn[]
 | 
			
		||||
  Upload   Upload[]   @relation(name: "Uploads")
 | 
			
		||||
  PollVote PollVote[]
 | 
			
		||||
  Earn        Earn[]
 | 
			
		||||
  Upload      Upload[]      @relation(name: "Uploads")
 | 
			
		||||
  PollVote    PollVote[]
 | 
			
		||||
  Donation    Donation[]
 | 
			
		||||
  ReferralAct ReferralAct[]
 | 
			
		||||
 | 
			
		||||
  @@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")
 | 
			
		||||
@ -305,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
 | 
			
		||||
@ -316,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])
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								svgs/share-fill.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								svgs/share-fill.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13.576 17.271l-5.11-2.787a3.5 3.5 0 1 1 0-4.968l5.11-2.787a3.5 3.5 0 1 1 .958 1.755l-5.11 2.787a3.514 3.514 0 0 1 0 1.458l5.11 2.787a3.5 3.5 0 1 1-.958 1.755z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 297 B  | 
							
								
								
									
										1
									
								
								svgs/share-forward-fill.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								svgs/share-forward-fill.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13 14h-2a8.999 8.999 0 0 0-7.968 4.81A10.136 10.136 0 0 1 3 18C3 12.477 7.477 8 13 8V3l10 8-10 8v-5z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 239 B  | 
@ -10,13 +10,20 @@ 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 - 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'`
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user