Merge branch 'master' into 266-zaps-without-account
This commit is contained in:
		
						commit
						3d0bb4b32c
					
				
							
								
								
									
										6
									
								
								.ebextensions/autoscaling.config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.ebextensions/autoscaling.config
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
Resources:
 | 
			
		||||
  AWSEBAutoScalingGroup:
 | 
			
		||||
    Type: "AWS::AutoScaling::AutoScalingGroup"
 | 
			
		||||
    Properties:
 | 
			
		||||
      HealthCheckType: ELB
 | 
			
		||||
      HealthCheckGracePeriod: 300
 | 
			
		||||
@ -4,4 +4,4 @@ echo primsa migrate
 | 
			
		||||
npm run migrate
 | 
			
		||||
 | 
			
		||||
echo build with npm
 | 
			
		||||
npm run build
 | 
			
		||||
sudo -E -u webapp npm run build
 | 
			
		||||
@ -4,4 +4,4 @@ echo primsa migrate
 | 
			
		||||
npm run migrate
 | 
			
		||||
 | 
			
		||||
echo build with npm
 | 
			
		||||
npm run build
 | 
			
		||||
sudo -E -u webapp npm run build
 | 
			
		||||
@ -28,9 +28,9 @@ http {
 | 
			
		||||
        listen        8008 default_server;
 | 
			
		||||
        access_log    /var/log/nginx/access.log main;
 | 
			
		||||
 | 
			
		||||
        client_header_timeout 60;
 | 
			
		||||
        client_body_timeout   60;
 | 
			
		||||
        keepalive_timeout     60;
 | 
			
		||||
        client_header_timeout 90;
 | 
			
		||||
        client_body_timeout   90;
 | 
			
		||||
        keepalive_timeout     90;
 | 
			
		||||
        gzip                  on;
 | 
			
		||||
        gzip_comp_level       4;
 | 
			
		||||
        gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
# syntax=docker/dockerfile:1
 | 
			
		||||
 | 
			
		||||
FROM node:16.16.0-bullseye
 | 
			
		||||
FROM node:18.17.0-bullseye
 | 
			
		||||
 | 
			
		||||
ENV NODE_ENV=development
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,8 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (!lastReward) return { total: 0, sources: [] }
 | 
			
		||||
 | 
			
		||||
      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)),
 | 
			
		||||
 | 
			
		||||
@ -35,17 +35,17 @@ async function serialize (models, ...calls) {
 | 
			
		||||
      if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
 | 
			
		||||
        bail(new Error('faucet has been revoked or is exhausted'))
 | 
			
		||||
      }
 | 
			
		||||
      if (error.message.includes('23514')) {
 | 
			
		||||
        bail(new Error('constraint failure'))
 | 
			
		||||
      }
 | 
			
		||||
      if (error.message.includes('SN_INV_PENDING_LIMIT')) {
 | 
			
		||||
        bail(new Error('too many pending invoices'))
 | 
			
		||||
      }
 | 
			
		||||
      if (error.message.includes('SN_INV_EXCEED_BALANCE')) {
 | 
			
		||||
        bail(new Error('pending invoices must not cause balance to exceed 1m sats'))
 | 
			
		||||
      }
 | 
			
		||||
      if (error.message.includes('40001')) {
 | 
			
		||||
        throw new Error('wallet balance serialization failure - retry again')
 | 
			
		||||
      if (error.message.includes('40001') || error.code === 'P2034') {
 | 
			
		||||
        throw new Error('wallet balance serialization failure - try again')
 | 
			
		||||
      }
 | 
			
		||||
      if (error.message.includes('23514') || ['P2002', 'P2003', 'P2004'].includes(error.code)) {
 | 
			
		||||
        bail(new Error('constraint failure'))
 | 
			
		||||
      }
 | 
			
		||||
      bail(error)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -172,7 +172,7 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
        switch (f.type) {
 | 
			
		||||
          case 'withdrawal':
 | 
			
		||||
            f.msats = (-1 * f.msats) - f.msatsFee
 | 
			
		||||
            f.msats = (-1 * Number(f.msats)) - Number(f.msatsFee)
 | 
			
		||||
            break
 | 
			
		||||
          case 'spent':
 | 
			
		||||
            f.msats *= -1
 | 
			
		||||
 | 
			
		||||
@ -38,14 +38,14 @@ export default async function getSSRApolloClient ({ req, res, me = null }) {
 | 
			
		||||
    assumeImmutableResults: true,
 | 
			
		||||
    defaultOptions: {
 | 
			
		||||
      watchQuery: {
 | 
			
		||||
        fetchPolicy: 'cache-only',
 | 
			
		||||
        nextFetchPolicy: 'cache-only',
 | 
			
		||||
        fetchPolicy: 'no-cache',
 | 
			
		||||
        nextFetchPolicy: 'no-cache',
 | 
			
		||||
        canonizeResults: true,
 | 
			
		||||
        ssr: true
 | 
			
		||||
      },
 | 
			
		||||
      query: {
 | 
			
		||||
        fetchPolicy: 'cache-first',
 | 
			
		||||
        nextFetchPolicy: 'cache-only',
 | 
			
		||||
        fetchPolicy: 'no-cache',
 | 
			
		||||
        nextFetchPolicy: 'no-cache',
 | 
			
		||||
        canonizeResults: true,
 | 
			
		||||
        ssr: true
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -89,7 +89,7 @@ export function WhenAreaChart ({ data }) {
 | 
			
		||||
          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)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--bs-body-color)', backgroundColor: 'var(--bs-body-bg)' }} />
 | 
			
		||||
        <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]} />)}
 | 
			
		||||
@ -124,7 +124,7 @@ export function WhenLineChart ({ data }) {
 | 
			
		||||
          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)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--bs-body-color)', backgroundColor: 'var(--bs-body-bg)' }} />
 | 
			
		||||
        <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]} />)}
 | 
			
		||||
@ -160,7 +160,7 @@ export function WhenComposedChart ({ data, lineNames, areaNames, barNames }) {
 | 
			
		||||
        />
 | 
			
		||||
        <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)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter(when)} contentStyle={{ color: 'var(--bs-body-color)', backgroundColor: 'var(--bs-body-bg)' }} />
 | 
			
		||||
        <Legend />
 | 
			
		||||
        {barNames?.map((v, i) =>
 | 
			
		||||
          <Bar yAxisId='right' key={v} type='monotone' dataKey={v} name={v} stroke='var(--bs-info)' fill='var(--bs-info)' />)}
 | 
			
		||||
 | 
			
		||||
@ -114,15 +114,19 @@ export default function Comment ({
 | 
			
		||||
    if (Number(router.query.commentId) === Number(item.id)) {
 | 
			
		||||
      // HACK wait for other comments to collapse if they're collapsed
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        ref.current.scrollIntoView()
 | 
			
		||||
        ref.current.classList.add('flash-it')
 | 
			
		||||
        router.replace({
 | 
			
		||||
          pathname: router.pathname,
 | 
			
		||||
          query: { id: router.query.id }
 | 
			
		||||
        }, undefined, { scroll: false })
 | 
			
		||||
        ref.current.scrollIntoView({ behavior: 'instant', block: 'start' })
 | 
			
		||||
        ref.current.classList.add('outline-it')
 | 
			
		||||
      }, 20)
 | 
			
		||||
    }
 | 
			
		||||
  }, [item])
 | 
			
		||||
  }, [item.id, router.query.commentId])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (router.query.commentsViewedAt &&
 | 
			
		||||
        me?.id !== item.user?.id &&
 | 
			
		||||
        new Date(item.createdAt).getTime() > router.query.commentsViewedAt) {
 | 
			
		||||
      ref.current.classList.add('outline-new-comment')
 | 
			
		||||
    }
 | 
			
		||||
  }, [item.id])
 | 
			
		||||
 | 
			
		||||
  const bottomedOut = depth === COMMENT_DEPTH_LIMIT
 | 
			
		||||
  const op = root.user.name === item.user.name
 | 
			
		||||
@ -131,6 +135,8 @@ export default function Comment ({
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse === 'yep' ? styles.collapsed : ''}`}
 | 
			
		||||
      onMouseEnter={() => ref.current.classList.add('outline-new-comment-unset')}
 | 
			
		||||
      onTouchStart={() => ref.current.classList.add('outline-new-comment-unset')}
 | 
			
		||||
    >
 | 
			
		||||
      <div className={`${itemStyles.item} ${styles.item}`}>
 | 
			
		||||
        {item.meDontLike
 | 
			
		||||
 | 
			
		||||
@ -73,7 +73,7 @@ export default function Comments ({ parentId, pinned, bio, parentCreatedAt, comm
 | 
			
		||||
              router.push({
 | 
			
		||||
                pathname: router.pathname,
 | 
			
		||||
                query: { ...router.query, sort }
 | 
			
		||||
              }, router.asPath, { scroll: false })
 | 
			
		||||
              }, undefined, { scroll: false })
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        : null}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,54 @@
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { getTheme, listenForThemeChange, setTheme } from '../public/dark'
 | 
			
		||||
 | 
			
		||||
const handleThemeChange = (dark) => {
 | 
			
		||||
  const root = window.document.documentElement
 | 
			
		||||
  root.setAttribute('data-bs-theme', dark ? 'dark' : 'light')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const STORAGE_KEY = 'darkMode'
 | 
			
		||||
const PREFER_DARK_QUERY = '(prefers-color-scheme: dark)'
 | 
			
		||||
 | 
			
		||||
const getTheme = () => {
 | 
			
		||||
  const mql = window.matchMedia(PREFER_DARK_QUERY)
 | 
			
		||||
  const supportsColorSchemeQuery = mql.media === PREFER_DARK_QUERY
 | 
			
		||||
  let localStorageTheme = null
 | 
			
		||||
  try {
 | 
			
		||||
    localStorageTheme = window.localStorage.getItem(STORAGE_KEY)
 | 
			
		||||
  } catch (err) {}
 | 
			
		||||
  const localStorageExists = localStorageTheme !== null
 | 
			
		||||
  if (localStorageExists) {
 | 
			
		||||
    localStorageTheme = JSON.parse(localStorageTheme)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (localStorageExists) {
 | 
			
		||||
    return { user: true, dark: localStorageTheme }
 | 
			
		||||
  } else if (supportsColorSchemeQuery) {
 | 
			
		||||
    return { user: false, dark: mql.matches }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const setTheme = (dark) => {
 | 
			
		||||
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(dark))
 | 
			
		||||
  handleThemeChange(dark)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const listenForThemeChange = (onChange) => {
 | 
			
		||||
  const mql = window.matchMedia(PREFER_DARK_QUERY)
 | 
			
		||||
  mql.onchange = mql => {
 | 
			
		||||
    const { user, dark } = getTheme()
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      handleThemeChange(dark)
 | 
			
		||||
      onChange({ user, dark })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  window.onstorage = e => {
 | 
			
		||||
    if (e.key === STORAGE_KEY) {
 | 
			
		||||
      const dark = JSON.parse(e.newValue)
 | 
			
		||||
      setTheme(dark)
 | 
			
		||||
      onChange({ user: true, dark })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function useDarkMode () {
 | 
			
		||||
  const [dark, setDark] = useState()
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import styles from './fee-button.module.css'
 | 
			
		||||
import { gql, useQuery } from '@apollo/client'
 | 
			
		||||
import { useFormikContext } from 'formik'
 | 
			
		||||
import { useMe } from './me'
 | 
			
		||||
import { ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
 | 
			
		||||
import { SSR, ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
 | 
			
		||||
@ -48,7 +48,7 @@ export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton,
 | 
			
		||||
  const query = parentId
 | 
			
		||||
    ? gql`{ itemRepetition(parentId: "${parentId}") }`
 | 
			
		||||
    : gql`{ itemRepetition }`
 | 
			
		||||
  const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  const repetition = me ? data?.itemRepetition || 0 : 0
 | 
			
		||||
  const formik = useFormikContext()
 | 
			
		||||
  const boost = Number(formik?.values?.boost) || 0
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,8 @@
 | 
			
		||||
 | 
			
		||||
.receipt td {
 | 
			
		||||
    padding: .25rem .1rem;
 | 
			
		||||
    background-color: var(--theme-inputBg);
 | 
			
		||||
    color: var(--bs-body-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.receipt tfoot {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { gql, useQuery } from '@apollo/client'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { RewardLine } from '../pages/rewards'
 | 
			
		||||
import { SSR } from '../lib/constants'
 | 
			
		||||
 | 
			
		||||
const REWARDS = gql`
 | 
			
		||||
{
 | 
			
		||||
@ -10,7 +11,7 @@ const REWARDS = gql`
 | 
			
		||||
}`
 | 
			
		||||
 | 
			
		||||
export default function Rewards () {
 | 
			
		||||
  const { data } = useQuery(REWARDS, { pollInterval: 60000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  const { data } = useQuery(REWARDS, SSR ? { ssr: false } : { pollInterval: 60000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  const total = data?.expectedRewards?.total
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -11,13 +11,13 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.contrastLink {
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  color: var(--bs-body-color);
 | 
			
		||||
}
 | 
			
		||||
.contrastLink:hover {
 | 
			
		||||
    color: var(--theme-color);
 | 
			
		||||
    color: var(--bs-body-color);
 | 
			
		||||
}
 | 
			
		||||
.contrastLink svg {
 | 
			
		||||
    fill: var(--theme-color);
 | 
			
		||||
    fill: var(--bs-body-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.version {
 | 
			
		||||
 | 
			
		||||
@ -72,7 +72,7 @@ export function InputSkeleton ({ label, hint }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <BootstrapForm.Group>
 | 
			
		||||
      {label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
 | 
			
		||||
      <div className='form-control clouds' />
 | 
			
		||||
      <div className='form-control clouds' style={{ color: 'transparent' }}>.</div>
 | 
			
		||||
      {hint &&
 | 
			
		||||
        <BootstrapForm.Text>
 | 
			
		||||
          {hint}
 | 
			
		||||
@ -431,7 +431,7 @@ export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra,
 | 
			
		||||
            handleChange && handleChange(e.target.checked, helpers.setValue)
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <BootstrapForm.Check.Label className={'d-flex' + (disabled ? ' text-muted' : '')}>
 | 
			
		||||
        <BootstrapForm.Check.Label className={'d-inline-flex flex-nowrap align-items-center' + (disabled ? ' text-muted' : '')}>
 | 
			
		||||
          <div className='flex-grow-1'>{label}</div>
 | 
			
		||||
          {extra &&
 | 
			
		||||
            <div className={styles.checkboxExtra}>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ import Price from './price'
 | 
			
		||||
import { useMe } from './me'
 | 
			
		||||
import Head from 'next/head'
 | 
			
		||||
import { signOut } from 'next-auth/react'
 | 
			
		||||
import { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { useCallback, useEffect } from 'react'
 | 
			
		||||
import { randInRange } from '../lib/rand'
 | 
			
		||||
import { abbrNum } from '../lib/format'
 | 
			
		||||
import NoteIcon from '../svgs/notification-4-fill.svg'
 | 
			
		||||
@ -20,7 +20,7 @@ import CowboyHat from './cowboy-hat'
 | 
			
		||||
import { Select } from './form'
 | 
			
		||||
import SearchIcon from '../svgs/search-line.svg'
 | 
			
		||||
import BackArrow from '../svgs/arrow-left-line.svg'
 | 
			
		||||
import { SUBS } from '../lib/constants'
 | 
			
		||||
import { SSR, SUBS } from '../lib/constants'
 | 
			
		||||
import { useLightning } from './lightning'
 | 
			
		||||
import { HAS_NOTIFICATIONS } from '../fragments/notifications'
 | 
			
		||||
 | 
			
		||||
@ -31,24 +31,28 @@ function WalletSummary ({ me }) {
 | 
			
		||||
 | 
			
		||||
function Back () {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const [show, setShow] = useState()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setShow(typeof window !== 'undefined' && router.asPath !== '/' &&
 | 
			
		||||
    (typeof window.navigation === 'undefined' || window.navigation.canGoBack === undefined || window?.navigation.canGoBack))
 | 
			
		||||
  }, [router.asPath])
 | 
			
		||||
 | 
			
		||||
  if (show) {
 | 
			
		||||
    return <a role='button' tabIndex='0' className='nav-link standalone p-0' onClick={() => router.back()}><BackArrow className='theme me-1 me-md-2' width={22} height={22} /></a>
 | 
			
		||||
  }
 | 
			
		||||
  return null
 | 
			
		||||
  return router.asPath !== '/' &&
 | 
			
		||||
    <a
 | 
			
		||||
      role='button' tabIndex='0' className='nav-link standalone p-0' onClick={() => {
 | 
			
		||||
        if (typeof window.navigation === 'undefined' || window.navigation.canGoBack === undefined || window?.navigation.canGoBack) {
 | 
			
		||||
          router.back()
 | 
			
		||||
        } else {
 | 
			
		||||
          router.push('/')
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <BackArrow className='theme me-1 me-md-2' width={22} height={22} />
 | 
			
		||||
    </a>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function NotificationBell () {
 | 
			
		||||
  const { data } = useQuery(HAS_NOTIFICATIONS, {
 | 
			
		||||
    pollInterval: 30000,
 | 
			
		||||
    nextFetchPolicy: 'cache-and-network'
 | 
			
		||||
  })
 | 
			
		||||
  const { data } = useQuery(HAS_NOTIFICATIONS, SSR
 | 
			
		||||
    ? {}
 | 
			
		||||
    : {
 | 
			
		||||
        pollInterval: 30000,
 | 
			
		||||
        nextFetchPolicy: 'cache-and-network'
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@
 | 
			
		||||
    background-color: var(--bs-primary);
 | 
			
		||||
    top: 3px;
 | 
			
		||||
    right: 0px;
 | 
			
		||||
    border: 1px solid var(--theme-body);
 | 
			
		||||
    border: 1px solid var(--bs-body-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.notification {
 | 
			
		||||
@ -47,7 +47,7 @@
 | 
			
		||||
    background-color: var(--bs-danger);
 | 
			
		||||
    top: 1px;
 | 
			
		||||
    right: 8px;
 | 
			
		||||
    border: 1px solid var(--theme-body);
 | 
			
		||||
    border: 1px solid var(--bs-body-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbarNav {
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import Badge from 'react-bootstrap/Badge'
 | 
			
		||||
import Dropdown from 'react-bootstrap/Dropdown'
 | 
			
		||||
import Countdown from './countdown'
 | 
			
		||||
import { abbrNum } from '../lib/format'
 | 
			
		||||
import { newComments } from '../lib/new-comments'
 | 
			
		||||
import { newComments, commentsViewedAt } from '../lib/new-comments'
 | 
			
		||||
import { timeSince } from '../lib/time'
 | 
			
		||||
import CowboyHat from './cowboy-hat'
 | 
			
		||||
import { DeleteDropdownItem } from './delete'
 | 
			
		||||
@ -48,9 +48,22 @@ export default function ItemInfo ({ item, pendingSats, full, commentsText, class
 | 
			
		||||
          <span>{abbrNum(item.boost)} boost</span>
 | 
			
		||||
          <span> \ </span>
 | 
			
		||||
        </>}
 | 
			
		||||
      <Link href={`/items/${item.id}`} title={`${item.commentSats} sats`} className='text-reset'>
 | 
			
		||||
      <Link
 | 
			
		||||
        href={`/items/${item.id}`} onClick={(e) => {
 | 
			
		||||
          const viewedAt = commentsViewedAt(item)
 | 
			
		||||
          if (viewedAt) {
 | 
			
		||||
            e.preventDefault()
 | 
			
		||||
            router.push(
 | 
			
		||||
              `/items/${item.id}?commentsViewedAt=${viewedAt}`,
 | 
			
		||||
              `/items/${item.id}`)
 | 
			
		||||
          }
 | 
			
		||||
        }} title={`${item.commentSats} sats`} className='text-reset position-relative'
 | 
			
		||||
      >
 | 
			
		||||
        {item.ncomments} {commentsText || 'comments'}
 | 
			
		||||
        {hasNewComments && <>{' '}<Badge className={styles.newComment} bg={null}>new</Badge></>}
 | 
			
		||||
        {hasNewComments &&
 | 
			
		||||
          <span className={styles.notification}>
 | 
			
		||||
            <span className='invisible'>{' '}</span>
 | 
			
		||||
          </span>}
 | 
			
		||||
      </Link>
 | 
			
		||||
      <span> \ </span>
 | 
			
		||||
      <span>
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@ export default function ItemJob ({ item, toc, rank, children }) {
 | 
			
		||||
                @{item.user.name}<CowboyHat className='ms-1 fill-grey' user={item.user} height={12} width={12} />
 | 
			
		||||
              </Link>
 | 
			
		||||
              <span> </span>
 | 
			
		||||
              <Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset'>
 | 
			
		||||
              <Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
 | 
			
		||||
                {timeSince(new Date(item.createdAt))}
 | 
			
		||||
              </Link>
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,8 @@ import Flag from '../svgs/flag-fill.svg'
 | 
			
		||||
import ImageIcon from '../svgs/image-fill.svg'
 | 
			
		||||
import { abbrNum } from '../lib/format'
 | 
			
		||||
import ItemInfo from './item-info'
 | 
			
		||||
import { commentsViewedAt } from '../lib/new-comments'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
 | 
			
		||||
export function SearchTitle ({ title }) {
 | 
			
		||||
  return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
 | 
			
		||||
@ -21,6 +23,7 @@ export function SearchTitle ({ title }) {
 | 
			
		||||
 | 
			
		||||
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments }) {
 | 
			
		||||
  const titleRef = useRef()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const [pendingSats, setPendingSats] = useState(0)
 | 
			
		||||
 | 
			
		||||
  const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
 | 
			
		||||
@ -39,7 +42,18 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
 | 
			
		||||
          : item.meDontLike ? <Flag width={24} height={24} className={styles.dontLike} /> : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
 | 
			
		||||
        <div className={styles.hunk}>
 | 
			
		||||
          <div className={`${styles.main} flex-wrap`}>
 | 
			
		||||
            <Link href={`/items/${item.id}`} ref={titleRef} className={`${styles.title} text-reset me-2`}>
 | 
			
		||||
            <Link
 | 
			
		||||
              href={`/items/${item.id}`}
 | 
			
		||||
              onClick={(e) => {
 | 
			
		||||
                const viewedAt = commentsViewedAt(item)
 | 
			
		||||
                if (viewedAt) {
 | 
			
		||||
                  e.preventDefault()
 | 
			
		||||
                  router.push(
 | 
			
		||||
                    `/items/${item.id}?commentsViewedAt=${viewedAt}`,
 | 
			
		||||
                    `/items/${item.id}`)
 | 
			
		||||
                }
 | 
			
		||||
              }} ref={titleRef} className={`${styles.title} text-reset me-2`}
 | 
			
		||||
            >
 | 
			
		||||
              {item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
 | 
			
		||||
              {item.pollCost && <span className={styles.icon}> <PollIcon className='fill-grey ms-1' height={14} width={14} /></span>}
 | 
			
		||||
              {item.bounty > 0 &&
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,15 @@
 | 
			
		||||
    padding-bottom: .15rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.notification {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    padding: 3px;
 | 
			
		||||
    background-color: var(--bs-info);
 | 
			
		||||
    top: -3px;
 | 
			
		||||
    right: -4px;
 | 
			
		||||
    border: 1px solid var(--bs-body-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,19 +8,21 @@ import { CommentFlat } from './comment'
 | 
			
		||||
import { SUB_ITEMS } from '../fragments/subs'
 | 
			
		||||
import { LIMIT } from '../lib/cursor'
 | 
			
		||||
import ItemFull from './item-full'
 | 
			
		||||
import { useData } from './use-data'
 | 
			
		||||
 | 
			
		||||
export default function Items ({ ssrData, variables = {}, query, destructureData, rank, noMoreText, Footer, filter = () => true }) {
 | 
			
		||||
  const { data, fetchMore } = useQuery(query || SUB_ITEMS, { variables })
 | 
			
		||||
  const Foooter = Footer || MoreFooter
 | 
			
		||||
  const dat = useData(data, ssrData)
 | 
			
		||||
 | 
			
		||||
  const { items, pins, cursor } = useMemo(() => {
 | 
			
		||||
    if (!data && !ssrData) return {}
 | 
			
		||||
    if (!dat) return {}
 | 
			
		||||
    if (destructureData) {
 | 
			
		||||
      return destructureData(data || ssrData)
 | 
			
		||||
      return destructureData(dat)
 | 
			
		||||
    } else {
 | 
			
		||||
      return data?.items || ssrData?.items
 | 
			
		||||
      return dat?.items
 | 
			
		||||
    }
 | 
			
		||||
  }, [data, ssrData])
 | 
			
		||||
  }, [dat])
 | 
			
		||||
 | 
			
		||||
  const pinMap = useMemo(() =>
 | 
			
		||||
    pins?.reduce((a, p) => { a[p.position] = p; return a }, {}), [pins])
 | 
			
		||||
@ -28,7 +30,7 @@ export default function Items ({ ssrData, variables = {}, query, destructureData
 | 
			
		||||
  const Skeleton = useCallback(() =>
 | 
			
		||||
    <ItemsSkeleton rank={rank} startRank={items?.length} limit={variables.limit} />, [rank, items])
 | 
			
		||||
 | 
			
		||||
  if (!ssrData && !data) {
 | 
			
		||||
  if (!dat) {
 | 
			
		||||
    return <Skeleton />
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import Qr, { QrSkeleton } from './qr'
 | 
			
		||||
import styles from './lightning-auth.module.css'
 | 
			
		||||
import BackIcon from '../svgs/arrow-left-line.svg'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { SSR } from '../lib/constants'
 | 
			
		||||
 | 
			
		||||
function QrAuth ({ k1, encodedUrl, slashtagUrl, callbackUrl }) {
 | 
			
		||||
  const query = gql`
 | 
			
		||||
@ -18,7 +19,7 @@ function QrAuth ({ k1, encodedUrl, slashtagUrl, callbackUrl }) {
 | 
			
		||||
      k1
 | 
			
		||||
    }
 | 
			
		||||
  }`
 | 
			
		||||
  const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (data?.lnAuth?.pubkey) {
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ const authErrorMessages = {
 | 
			
		||||
  default: 'Auth failed. Try again or choose a different method.'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function authErrorMessage(error) {
 | 
			
		||||
export function authErrorMessage (error) {
 | 
			
		||||
  return error && (authErrorMessages[error] ?? authErrorMessages.default)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -85,10 +85,13 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
 | 
			
		||||
                className={`mt-2 ${styles.providerButton}`}
 | 
			
		||||
                key={provider.id}
 | 
			
		||||
                type={provider.id.toLowerCase()}
 | 
			
		||||
                onClick={() => router.push({
 | 
			
		||||
                  pathname: router.pathname,
 | 
			
		||||
                  query: { callbackUrl: router.query.callbackUrl, type: provider.name.toLowerCase() }
 | 
			
		||||
                })}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  const { nodata, ...query } = router.query
 | 
			
		||||
                  router.push({
 | 
			
		||||
                    pathname: router.pathname,
 | 
			
		||||
                    query: { ...query, type: provider.name.toLowerCase() }
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
                text={`${text || 'Login'} with`}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,14 @@
 | 
			
		||||
import React, { useContext } from 'react'
 | 
			
		||||
import { useQuery } from '@apollo/client'
 | 
			
		||||
import { ME } from '../fragments/users'
 | 
			
		||||
import { SSR } from '../lib/constants'
 | 
			
		||||
 | 
			
		||||
export const MeContext = React.createContext({
 | 
			
		||||
  me: null
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export function MeProvider ({ me, children }) {
 | 
			
		||||
  const { data } = useQuery(ME, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  const { data } = useQuery(ME, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
 | 
			
		||||
  const contextValue = {
 | 
			
		||||
    me: data?.me || me
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { useState, useEffect, useMemo } from 'react'
 | 
			
		||||
import { useApolloClient, useQuery } from '@apollo/client'
 | 
			
		||||
import { useQuery } from '@apollo/client'
 | 
			
		||||
import Comment, { CommentSkeleton } from './comment'
 | 
			
		||||
import Item from './item'
 | 
			
		||||
import ItemJob from './item-job'
 | 
			
		||||
import { HAS_NOTIFICATIONS, NOTIFICATIONS } from '../fragments/notifications'
 | 
			
		||||
import { NOTIFICATIONS } from '../fragments/notifications'
 | 
			
		||||
import MoreFooter from './more-footer'
 | 
			
		||||
import Invite from './invite'
 | 
			
		||||
import { ignoreClick } from '../lib/clicks'
 | 
			
		||||
@ -20,29 +20,47 @@ import styles from './notifications.module.css'
 | 
			
		||||
import { useServiceWorker } from './serviceworker'
 | 
			
		||||
import { Checkbox, Form } from './form'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { useData } from './use-data'
 | 
			
		||||
 | 
			
		||||
function Notification ({ n }) {
 | 
			
		||||
  switch (n.__typename) {
 | 
			
		||||
    case 'Earn': return <EarnNotification n={n} />
 | 
			
		||||
    case 'Invitification': return <Invitification n={n} />
 | 
			
		||||
    case 'InvoicePaid': return <InvoicePaid n={n} />
 | 
			
		||||
    case 'Referral': return <Referral n={n} />
 | 
			
		||||
    case 'Streak': return <Streak n={n} />
 | 
			
		||||
    case 'Votification': return <Votification n={n} />
 | 
			
		||||
    case 'Mention': return <Mention n={n} />
 | 
			
		||||
    case 'JobChanged': return <JobChanged n={n} />
 | 
			
		||||
    case 'Reply': return <Reply n={n} />
 | 
			
		||||
  }
 | 
			
		||||
  console.error('__typename not supported:', n.__typename)
 | 
			
		||||
  return null
 | 
			
		||||
function Notification ({ n, fresh }) {
 | 
			
		||||
  const type = n.__typename
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationLayout nid={nid(n)} {...defaultOnClick(n)} fresh={fresh}>
 | 
			
		||||
      {
 | 
			
		||||
        (type === 'Earn' && <EarnNotification n={n} />) ||
 | 
			
		||||
        (type === 'Invitification' && <Invitification n={n} />) ||
 | 
			
		||||
        (type === 'InvoicePaid' && <InvoicePaid n={n} />) ||
 | 
			
		||||
        (type === 'Referral' && <Referral n={n} />) ||
 | 
			
		||||
        (type === 'Streak' && <Streak n={n} />) ||
 | 
			
		||||
        (type === 'Votification' && <Votification n={n} />) ||
 | 
			
		||||
        (type === 'Mention' && <Mention n={n} />) ||
 | 
			
		||||
        (type === 'JobChanged' && <JobChanged n={n} />) ||
 | 
			
		||||
        (type === 'Reply' && <Reply n={n} />)
 | 
			
		||||
      }
 | 
			
		||||
    </NotificationLayout>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function NotificationLayout ({ children, href, as }) {
 | 
			
		||||
function NotificationLayout ({ children, nid, href, as, fresh }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  if (!href) return <div className={fresh ? styles.fresh : ''}>{children}</div>
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className='clickToContext'
 | 
			
		||||
      onClick={(e) => !ignoreClick(e) && router.push(href, as)}
 | 
			
		||||
      className={
 | 
			
		||||
        `clickToContext ${fresh ? styles.fresh : ''} ${router?.query?.nid === nid ? 'outline-it' : ''}`
 | 
			
		||||
      }
 | 
			
		||||
      onClick={async (e) => {
 | 
			
		||||
        if (ignoreClick(e)) return
 | 
			
		||||
        nid && await router.replace({
 | 
			
		||||
          pathname: router.pathname,
 | 
			
		||||
          query: {
 | 
			
		||||
            ...router.query,
 | 
			
		||||
            nid
 | 
			
		||||
          }
 | 
			
		||||
        }, router.asPath, { ...router.options, shallow: true })
 | 
			
		||||
        router.push(href, as)
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
@ -50,6 +68,14 @@ function NotificationLayout ({ children, href, as }) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOnClick = n => {
 | 
			
		||||
  const type = n.__typename
 | 
			
		||||
  if (type === 'Earn') return {}
 | 
			
		||||
  if (type === 'Invitification') return { href: '/invites' }
 | 
			
		||||
  if (type === 'InvoicePaid') return { href: `/invoices/${n.invoice.id}` }
 | 
			
		||||
  if (type === 'Referral') return { href: '/referrals/month' }
 | 
			
		||||
  if (type === 'Streak') return {}
 | 
			
		||||
 | 
			
		||||
  // Votification, Mention, JobChanged, Reply all have item
 | 
			
		||||
  if (!n.item.title) {
 | 
			
		||||
    const path = n.item.path.split('.')
 | 
			
		||||
    if (path.length > COMMENT_DEPTH_LIMIT + 1) {
 | 
			
		||||
@ -126,7 +152,7 @@ function EarnNotification ({ n }) {
 | 
			
		||||
      <HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} />
 | 
			
		||||
      <div className='ms-2'>
 | 
			
		||||
        <div className='fw-bold text-boost'>
 | 
			
		||||
          you stacked {n.earnedSats} sats in rewards<small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
          you stacked {n.earnedSats} sats in rewards<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
        </div>
 | 
			
		||||
        {n.sources &&
 | 
			
		||||
          <div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}>
 | 
			
		||||
@ -145,7 +171,7 @@ function EarnNotification ({ n }) {
 | 
			
		||||
 | 
			
		||||
function Invitification ({ n }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationLayout href='/invites'>
 | 
			
		||||
    <>
 | 
			
		||||
      <small className='fw-bold text-secondary ms-2'>
 | 
			
		||||
        your invite has been redeemed by {n.invite.invitees.length} stackers
 | 
			
		||||
      </small>
 | 
			
		||||
@ -157,35 +183,31 @@ function Invitification ({ n }) {
 | 
			
		||||
        }
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </NotificationLayout>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InvoicePaid ({ n }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationLayout href={`/invoices/${n.invoice.id}`}>
 | 
			
		||||
      <div className='fw-bold text-info ms-2 py-1'>
 | 
			
		||||
        <Check className='fill-info me-1' />{n.earnedSats} sats were deposited in your account
 | 
			
		||||
        <small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
      </div>
 | 
			
		||||
    </NotificationLayout>
 | 
			
		||||
    <div className='fw-bold text-info ms-2 py-1'>
 | 
			
		||||
      <Check className='fill-info me-1' />{n.earnedSats} sats were deposited in your account
 | 
			
		||||
      <small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Referral ({ n }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationLayout>
 | 
			
		||||
      <small className='fw-bold text-secondary ms-2'>
 | 
			
		||||
        someone joined via one of your <Link href='/referrals/month' className='text-reset'>referral links</Link>
 | 
			
		||||
        <small className='text-muted ms-1 fw-normal'>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
      </small>
 | 
			
		||||
    </NotificationLayout>
 | 
			
		||||
    <small className='fw-bold text-secondary ms-2'>
 | 
			
		||||
      someone joined via one of your referral links
 | 
			
		||||
      <small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
 | 
			
		||||
    </small>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Votification ({ n }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationLayout {...defaultOnClick(n)}>
 | 
			
		||||
    <>
 | 
			
		||||
      <small className='fw-bold text-success ms-2'>
 | 
			
		||||
        your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
 | 
			
		||||
      </small>
 | 
			
		||||
@ -200,13 +222,13 @@ function Votification ({ n }) {
 | 
			
		||||
            </div>
 | 
			
		||||
            )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </NotificationLayout>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Mention ({ n }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationLayout {...defaultOnClick(n)}>
 | 
			
		||||
    <>
 | 
			
		||||
      <small className='fw-bold text-info ms-2'>
 | 
			
		||||
        you were mentioned in
 | 
			
		||||
      </small>
 | 
			
		||||
@ -220,13 +242,13 @@ function Mention ({ n }) {
 | 
			
		||||
              </RootProvider>
 | 
			
		||||
            </div>)}
 | 
			
		||||
      </div>
 | 
			
		||||
    </NotificationLayout>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function JobChanged ({ n }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationLayout {...defaultOnClick(n)}>
 | 
			
		||||
    <>
 | 
			
		||||
      <small className={`fw-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ms-1`}>
 | 
			
		||||
        {n.item.status === 'ACTIVE'
 | 
			
		||||
          ? 'your job is active again'
 | 
			
		||||
@ -235,29 +257,27 @@ function JobChanged ({ n }) {
 | 
			
		||||
              : 'your job has been stopped')}
 | 
			
		||||
      </small>
 | 
			
		||||
      <ItemJob item={n.item} />
 | 
			
		||||
    </NotificationLayout>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Reply ({ n }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationLayout {...defaultOnClick(n)} rootText='replying on:'>
 | 
			
		||||
      <div className='py-2'>
 | 
			
		||||
        {n.item.title
 | 
			
		||||
          ? <Item item={n.item} />
 | 
			
		||||
          : (
 | 
			
		||||
            <div className='pb-2'>
 | 
			
		||||
              <RootProvider root={n.item.root}>
 | 
			
		||||
                <Comment item={n.item} noReply includeParent clickToContext rootText='replying on:' />
 | 
			
		||||
              </RootProvider>
 | 
			
		||||
            </div>
 | 
			
		||||
            )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </NotificationLayout>
 | 
			
		||||
    <div className='py-2'>
 | 
			
		||||
      {n.item.title
 | 
			
		||||
        ? <Item item={n.item} />
 | 
			
		||||
        : (
 | 
			
		||||
          <div className='pb-2'>
 | 
			
		||||
            <RootProvider root={n.item.root}>
 | 
			
		||||
              <Comment item={n.item} noReply includeParent clickToContext rootText='replying on:' />
 | 
			
		||||
            </RootProvider>
 | 
			
		||||
          </div>
 | 
			
		||||
          )}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function NotificationAlert () {
 | 
			
		||||
export function NotificationAlert () {
 | 
			
		||||
  const [showAlert, setShowAlert] = useState(false)
 | 
			
		||||
  const [hasSubscription, setHasSubscription] = useState(false)
 | 
			
		||||
  const [error, setError] = useState(null)
 | 
			
		||||
@ -316,47 +336,39 @@ function NotificationAlert () {
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const nid = n => n.__typename + n.id + n.sortTime
 | 
			
		||||
 | 
			
		||||
export default function Notifications ({ ssrData }) {
 | 
			
		||||
  const { data, fetchMore } = useQuery(NOTIFICATIONS)
 | 
			
		||||
  const client = useApolloClient()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const dat = useData(data, ssrData)
 | 
			
		||||
 | 
			
		||||
  const { notifications: { notifications, lastChecked, cursor } } = useMemo(() => {
 | 
			
		||||
    return dat || { notifications: {} }
 | 
			
		||||
  }, [dat])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    client.writeQuery({
 | 
			
		||||
      query: HAS_NOTIFICATIONS,
 | 
			
		||||
      data: {
 | 
			
		||||
        hasNewNotes: false
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }, [client])
 | 
			
		||||
    if (lastChecked && !router?.query?.checkedAt) {
 | 
			
		||||
      router.replace({
 | 
			
		||||
        pathname: router.pathname,
 | 
			
		||||
        query: {
 | 
			
		||||
          ...router.query,
 | 
			
		||||
          nodata: true, // make sure nodata is set so we don't fetch on back/forward
 | 
			
		||||
          checkedAt: lastChecked
 | 
			
		||||
        }
 | 
			
		||||
      }, router.asPath, { ...router.options, shallow: true })
 | 
			
		||||
    }
 | 
			
		||||
  }, [router, lastChecked])
 | 
			
		||||
 | 
			
		||||
  const { notifications: { notifications, earn, lastChecked, cursor } } = useMemo(() => {
 | 
			
		||||
    if (!data && !ssrData) return { notifications: {} }
 | 
			
		||||
    return data || ssrData
 | 
			
		||||
  }, [data, ssrData])
 | 
			
		||||
 | 
			
		||||
  const [fresh, old] = useMemo(() => {
 | 
			
		||||
    if (!notifications) return [[], []]
 | 
			
		||||
    return notifications.reduce((result, n) => {
 | 
			
		||||
      result[new Date(n.sortTime).getTime() > lastChecked ? 0 : 1].push(n)
 | 
			
		||||
      return result
 | 
			
		||||
    },
 | 
			
		||||
    [[], []])
 | 
			
		||||
  }, [notifications, lastChecked])
 | 
			
		||||
 | 
			
		||||
  if (!data && !ssrData) return <CommentsFlatSkeleton />
 | 
			
		||||
  if (!dat) return <CommentsFlatSkeleton />
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NotificationAlert />
 | 
			
		||||
      <div className='fresh'>
 | 
			
		||||
        {earn && <Notification n={earn} key='earn' />}
 | 
			
		||||
        {fresh.map((n, i) => (
 | 
			
		||||
          <Notification n={n} key={n.__typename + n.id + n.sortTime} />
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
      {old.map((n, i) => (
 | 
			
		||||
        <Notification n={n} key={n.__typename + n.id + n.sortTime} />
 | 
			
		||||
      ))}
 | 
			
		||||
      {notifications.map(n =>
 | 
			
		||||
        <Notification
 | 
			
		||||
          n={n} key={nid(n)}
 | 
			
		||||
          fresh={new Date(n.sortTime) > new Date(router?.query?.checkedAt)}
 | 
			
		||||
        />)}
 | 
			
		||||
      <MoreFooter cursor={cursor} count={notifications?.length} fetchMore={fetchMore} Skeleton={CommentsFlatSkeleton} noMoreText='NO MORE' />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,16 @@
 | 
			
		||||
.clickToContext {
 | 
			
		||||
    border-radius: .4rem;
 | 
			
		||||
    padding: .2rem 0;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clickToContext:hover {
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.03);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fresh {
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.03);
 | 
			
		||||
    border-radius: .4rem;
 | 
			
		||||
    background-color: rgba(128, 128, 128, 0.1);
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fresh:not(.fresh ~ .fresh) {
 | 
			
		||||
    border-top-left-radius: .4rem;
 | 
			
		||||
    border-top-right-radius: .4rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fresh:has(+ :not(.fresh)) {
 | 
			
		||||
    border-bottom-left-radius: .4rem;
 | 
			
		||||
    border-bottom-right-radius: .4rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.alertBtn {
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import { fixedDecimal } from '../lib/format'
 | 
			
		||||
import { useMe } from './me'
 | 
			
		||||
import { PRICE } from '../fragments/price'
 | 
			
		||||
import { CURRENCY_SYMBOLS } from '../lib/currency'
 | 
			
		||||
import { SSR } from '../lib/constants'
 | 
			
		||||
 | 
			
		||||
export const PriceContext = React.createContext({
 | 
			
		||||
  price: null,
 | 
			
		||||
@ -19,8 +20,12 @@ export function PriceProvider ({ price, children }) {
 | 
			
		||||
  const fiatCurrency = me?.fiatCurrency
 | 
			
		||||
  const { data } = useQuery(PRICE, {
 | 
			
		||||
    variables: { fiatCurrency },
 | 
			
		||||
    pollInterval: 30000,
 | 
			
		||||
    nextFetchPolicy: 'cache-and-network'
 | 
			
		||||
    ...(SSR
 | 
			
		||||
      ? {}
 | 
			
		||||
      : {
 | 
			
		||||
          pollInterval: 30000,
 | 
			
		||||
          nextFetchPolicy: 'cache-and-network'
 | 
			
		||||
        })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const contextValue = {
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
 | 
			
		||||
.searchSection.solid {
 | 
			
		||||
    pointer-events: auto;
 | 
			
		||||
    background: var(--theme-body);
 | 
			
		||||
    background: var(--bs-body-bg);
 | 
			
		||||
    box-shadow: 0 -4px 12px hsl(0deg 0% 59% / 10%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ export default function TopHeader ({ sub, cat }) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const what = cat
 | 
			
		||||
  const by = router.query.by || ''
 | 
			
		||||
  const by = router.query.by || (what === 'stackers' ? 'stacked' : 'votes')
 | 
			
		||||
  const when = router.query.when || ''
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cover {
 | 
			
		||||
    background: var(--theme-body);
 | 
			
		||||
    background: var(--bs-body-bg);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    mix-blend-mode: color;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								components/use-data.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								components/use-data.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { useEffect, useRef, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
  What we want is to use ssrData if it exists, until cache data changes
 | 
			
		||||
  ... this prevents item list jitter where the intially rendered items
 | 
			
		||||
  are stale until the cache is rewritten with incoming ssrData
 | 
			
		||||
*/
 | 
			
		||||
export function useData (data, ssrData) {
 | 
			
		||||
  // when fresh is true, it means data has been updated after the initial render and it's populated
 | 
			
		||||
  const [fresh, setFresh] = useState(false)
 | 
			
		||||
 | 
			
		||||
  // on first render, we want to use ssrData if it's available
 | 
			
		||||
  // it's only unavailable on back/forward navigation
 | 
			
		||||
  const ref = useRef(true)
 | 
			
		||||
  const firstRender = ref.current
 | 
			
		||||
  ref.current = false
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!firstRender && !fresh && data) setFresh(true)
 | 
			
		||||
  }, [data])
 | 
			
		||||
 | 
			
		||||
  // if we don't have data yet, use ssrData
 | 
			
		||||
  // if we have data, but it's not fresh, use ssrData
 | 
			
		||||
  // unless we don't have ssrData
 | 
			
		||||
  if (!data || (!fresh && ssrData)) return ssrData
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
@ -7,6 +7,7 @@ import userStyles from './user-header.module.css'
 | 
			
		||||
import { useEffect, useMemo, useState } from 'react'
 | 
			
		||||
import { useQuery } from '@apollo/client'
 | 
			
		||||
import MoreFooter from './more-footer'
 | 
			
		||||
import { useData } from './use-data'
 | 
			
		||||
 | 
			
		||||
// all of this nonsense is to show the stat we are sorting by first
 | 
			
		||||
const Stacked = ({ user }) => (<span>{abbrNum(user.stacked)} stacked</span>)
 | 
			
		||||
@ -37,26 +38,25 @@ function seperate (arr, seperator) {
 | 
			
		||||
 | 
			
		||||
export default function UserList ({ ssrData, query, variables, destructureData }) {
 | 
			
		||||
  const { data, fetchMore } = useQuery(query, { variables })
 | 
			
		||||
  const dat = useData(data, ssrData)
 | 
			
		||||
  const [statComps, setStatComps] = useState(seperate(STAT_COMPONENTS, Seperator))
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (variables?.by) {
 | 
			
		||||
      // shift the stat we are sorting by to the front
 | 
			
		||||
      const comps = [...STAT_COMPONENTS]
 | 
			
		||||
      setStatComps(seperate([...comps.splice(STAT_POS[variables.by], 1), ...comps], Seperator))
 | 
			
		||||
    }
 | 
			
		||||
    // shift the stat we are sorting by to the front
 | 
			
		||||
    const comps = [...STAT_COMPONENTS]
 | 
			
		||||
    setStatComps(seperate([...comps.splice(STAT_POS[variables.by || 0], 1), ...comps], Seperator))
 | 
			
		||||
  }, [variables?.by])
 | 
			
		||||
 | 
			
		||||
  const { users, cursor } = useMemo(() => {
 | 
			
		||||
    if (!data && !ssrData) return {}
 | 
			
		||||
    if (!dat) return {}
 | 
			
		||||
    if (destructureData) {
 | 
			
		||||
      return destructureData(data || ssrData)
 | 
			
		||||
      return destructureData(dat)
 | 
			
		||||
    } else {
 | 
			
		||||
      return data || ssrData
 | 
			
		||||
      return dat
 | 
			
		||||
    }
 | 
			
		||||
  }, [data, ssrData])
 | 
			
		||||
  }, [dat])
 | 
			
		||||
 | 
			
		||||
  if (!ssrData && !data) {
 | 
			
		||||
  if (!dat) {
 | 
			
		||||
    return <UsersSkeleton />
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
 | 
			
		||||
import { decodeCursor, LIMIT } from './cursor'
 | 
			
		||||
import { SSR } from './constants'
 | 
			
		||||
 | 
			
		||||
function isFirstPage (cursor, existingThings) {
 | 
			
		||||
  if (cursor) {
 | 
			
		||||
@ -12,7 +13,6 @@ function isFirstPage (cursor, existingThings) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SSR = typeof window === 'undefined'
 | 
			
		||||
const defaultFetchPolicy = SSR ? 'cache-only' : 'cache-first'
 | 
			
		||||
const defaultNextFetchPolicy = SSR ? 'cache-only' : 'cache-first'
 | 
			
		||||
 | 
			
		||||
@ -158,14 +158,18 @@ function getClient (uri) {
 | 
			
		||||
    assumeImmutableResults: true,
 | 
			
		||||
    defaultOptions: {
 | 
			
		||||
      watchQuery: {
 | 
			
		||||
        initialFetchPolicy: defaultFetchPolicy,
 | 
			
		||||
        fetchPolicy: defaultFetchPolicy,
 | 
			
		||||
        nextFetchPolicy: defaultNextFetchPolicy,
 | 
			
		||||
        canonizeResults: true
 | 
			
		||||
        canonizeResults: true,
 | 
			
		||||
        ssr: SSR
 | 
			
		||||
      },
 | 
			
		||||
      query: {
 | 
			
		||||
        initialFetchPolicy: defaultFetchPolicy,
 | 
			
		||||
        fetchPolicy: defaultFetchPolicy,
 | 
			
		||||
        nextFetchPolicy: defaultNextFetchPolicy,
 | 
			
		||||
        canonizeResults: true
 | 
			
		||||
        canonizeResults: true,
 | 
			
		||||
        ssr: SSR
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@ -45,3 +45,6 @@ module.exports = {
 | 
			
		||||
  ANON_POST_FEE: 1000,
 | 
			
		||||
  ANON_COMMENT_FEE: 100,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const OLD_ITEM_DAYS = 3
 | 
			
		||||
export const SSR = typeof window === 'undefined'
 | 
			
		||||
 | 
			
		||||
@ -14,13 +14,17 @@ export function commentsViewedAfterComment (rootId, createdAt) {
 | 
			
		||||
  window.localStorage.setItem(`${COMMENTS_NUM_PREFIX}:${rootId}`, existingRootComments + 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function commentsViewedAt (item) {
 | 
			
		||||
  return window.localStorage.getItem(`${COMMENTS_VIEW_PREFIX}:${item.id}`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function newComments (item) {
 | 
			
		||||
  if (!item.parentId) {
 | 
			
		||||
    const commentsViewedAt = window.localStorage.getItem(`${COMMENTS_VIEW_PREFIX}:${item.id}`)
 | 
			
		||||
    const commentsViewNum = window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${item.id}`)
 | 
			
		||||
    const viewedAt = commentsViewedAt(item)
 | 
			
		||||
    const viewNum = window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${item.id}`)
 | 
			
		||||
 | 
			
		||||
    if (commentsViewedAt && commentsViewNum) {
 | 
			
		||||
      return commentsViewedAt < new Date(item.lastCommentAt).getTime() || commentsViewNum < item.ncomments
 | 
			
		||||
    if (viewedAt && viewNum) {
 | 
			
		||||
      return viewedAt < new Date(item.lastCommentAt).getTime() || viewNum < item.ncomments
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,296 +0,0 @@
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
'use strict'
 | 
			
		||||
 | 
			
		||||
Object.defineProperty(exports, '__esModule', {
 | 
			
		||||
  value: true
 | 
			
		||||
})
 | 
			
		||||
exports.getCompoundId = getCompoundId
 | 
			
		||||
exports.Adapter = exports.PrismaLegacyAdapter = PrismaLegacyAdapter
 | 
			
		||||
 | 
			
		||||
const _crypto = require('crypto')
 | 
			
		||||
 | 
			
		||||
function getCompoundId (a, b) {
 | 
			
		||||
  return (0, _crypto.createHash)('sha256').update(`${a}:${b}`).digest('hex')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function PrismaLegacyAdapter (config) {
 | 
			
		||||
  const {
 | 
			
		||||
    prisma,
 | 
			
		||||
    modelMapping = {
 | 
			
		||||
      User: 'user',
 | 
			
		||||
      Account: 'account',
 | 
			
		||||
      Session: 'session',
 | 
			
		||||
      VerificationRequest: 'verificationRequest'
 | 
			
		||||
    }
 | 
			
		||||
  } = config
 | 
			
		||||
  const {
 | 
			
		||||
    User,
 | 
			
		||||
    Account,
 | 
			
		||||
    Session,
 | 
			
		||||
    VerificationRequest
 | 
			
		||||
  } = modelMapping
 | 
			
		||||
  return {
 | 
			
		||||
    async getAdapter ({
 | 
			
		||||
      session: {
 | 
			
		||||
        maxAge,
 | 
			
		||||
        updateAge
 | 
			
		||||
      },
 | 
			
		||||
      secret,
 | 
			
		||||
      ...appOptions
 | 
			
		||||
    }) {
 | 
			
		||||
      const sessionMaxAge = maxAge * 1000
 | 
			
		||||
      const sessionUpdateAge = updateAge * 1000
 | 
			
		||||
 | 
			
		||||
      const hashToken = token => (0, _crypto.createHash)('sha256').update(`${token}${secret}`).digest('hex')
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        displayName: 'PRISMA_LEGACY',
 | 
			
		||||
 | 
			
		||||
        createUser (profile) {
 | 
			
		||||
          let _profile$emailVerifie
 | 
			
		||||
 | 
			
		||||
          return prisma[User].create({
 | 
			
		||||
            data: {
 | 
			
		||||
              name: profile.name,
 | 
			
		||||
              email: profile.email,
 | 
			
		||||
              image: profile.image,
 | 
			
		||||
              emailVerified: (_profile$emailVerifie = profile.emailVerified) === null || _profile$emailVerifie === void 0 ? void 0 : _profile$emailVerifie.toISOString()
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        getUser (id) {
 | 
			
		||||
          return prisma[User].findUnique({
 | 
			
		||||
            where: {
 | 
			
		||||
              id: Number(id)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        getUserByEmail (email) {
 | 
			
		||||
          if (email) {
 | 
			
		||||
            return prisma[User].findUnique({
 | 
			
		||||
              where: {
 | 
			
		||||
                email
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return null
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async getUserByProviderAccountId (providerId, providerAccountId) {
 | 
			
		||||
          const account = await prisma[Account].findUnique({
 | 
			
		||||
            where: {
 | 
			
		||||
              compoundId: getCompoundId(providerId, providerAccountId)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          if (account) {
 | 
			
		||||
            return prisma[User].findUnique({
 | 
			
		||||
              where: {
 | 
			
		||||
                id: account.userId
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return null
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        updateUser (user) {
 | 
			
		||||
          const {
 | 
			
		||||
            id,
 | 
			
		||||
            name,
 | 
			
		||||
            email,
 | 
			
		||||
            image,
 | 
			
		||||
            emailVerified
 | 
			
		||||
          } = user
 | 
			
		||||
          return prisma[User].update({
 | 
			
		||||
            where: {
 | 
			
		||||
              id
 | 
			
		||||
            },
 | 
			
		||||
            data: {
 | 
			
		||||
              name,
 | 
			
		||||
              email,
 | 
			
		||||
              image,
 | 
			
		||||
              emailVerified: emailVerified === null || emailVerified === void 0 ? void 0 : emailVerified.toISOString()
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        deleteUser (userId) {
 | 
			
		||||
          return prisma[User].delete({
 | 
			
		||||
            where: {
 | 
			
		||||
              id: userId
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
 | 
			
		||||
          return prisma[Account].create({
 | 
			
		||||
            data: {
 | 
			
		||||
              accessToken,
 | 
			
		||||
              refreshToken,
 | 
			
		||||
              compoundId: getCompoundId(providerId, providerAccountId),
 | 
			
		||||
              providerAccountId: `${providerAccountId}`,
 | 
			
		||||
              providerId,
 | 
			
		||||
              providerType,
 | 
			
		||||
              accessTokenExpires,
 | 
			
		||||
              userId
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        unlinkAccount (_, providerId, providerAccountId) {
 | 
			
		||||
          return prisma[Account].delete({
 | 
			
		||||
            where: {
 | 
			
		||||
              compoundId: getCompoundId(providerId, providerAccountId)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        createSession (user) {
 | 
			
		||||
          let expires = null
 | 
			
		||||
 | 
			
		||||
          if (sessionMaxAge) {
 | 
			
		||||
            const dateExpires = new Date()
 | 
			
		||||
            dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
 | 
			
		||||
            expires = dateExpires.toISOString()
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return prisma[Session].create({
 | 
			
		||||
            data: {
 | 
			
		||||
              expires,
 | 
			
		||||
              userId: user.id,
 | 
			
		||||
              sessionToken: (0, _crypto.randomBytes)(32).toString('hex'),
 | 
			
		||||
              accessToken: (0, _crypto.randomBytes)(32).toString('hex')
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async getSession (sessionToken) {
 | 
			
		||||
          const session = await prisma[Session].findUnique({
 | 
			
		||||
            where: {
 | 
			
		||||
              sessionToken
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          if (session !== null && session !== void 0 && session.expires && new Date() > session.expires) {
 | 
			
		||||
            await prisma[Session].delete({
 | 
			
		||||
              where: {
 | 
			
		||||
                sessionToken
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
            return null
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return session
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        updateSession (session, force) {
 | 
			
		||||
          if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
 | 
			
		||||
            const dateSessionIsDueToBeUpdated = new Date(session.expires)
 | 
			
		||||
            dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
 | 
			
		||||
            dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
 | 
			
		||||
 | 
			
		||||
            if (new Date() > dateSessionIsDueToBeUpdated) {
 | 
			
		||||
              const newExpiryDate = new Date()
 | 
			
		||||
              newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
 | 
			
		||||
              session.expires = newExpiryDate
 | 
			
		||||
            } else if (!force) {
 | 
			
		||||
              return null
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            if (!force) {
 | 
			
		||||
              return null
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const {
 | 
			
		||||
            id,
 | 
			
		||||
            expires
 | 
			
		||||
          } = session
 | 
			
		||||
          return prisma[Session].update({
 | 
			
		||||
            where: {
 | 
			
		||||
              id
 | 
			
		||||
            },
 | 
			
		||||
            data: {
 | 
			
		||||
              expires: expires.toISOString()
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        deleteSession (sessionToken) {
 | 
			
		||||
          return prisma[Session].delete({
 | 
			
		||||
            where: {
 | 
			
		||||
              sessionToken
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async createVerificationRequest (identifier, url, token, _, provider) {
 | 
			
		||||
          const {
 | 
			
		||||
            sendVerificationRequest,
 | 
			
		||||
            maxAge
 | 
			
		||||
          } = provider
 | 
			
		||||
          let expires = null
 | 
			
		||||
 | 
			
		||||
          if (maxAge) {
 | 
			
		||||
            const dateExpires = new Date()
 | 
			
		||||
            dateExpires.setTime(dateExpires.getTime() + maxAge * 1000)
 | 
			
		||||
            expires = dateExpires.toISOString()
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const verificationRequest = await prisma[VerificationRequest].create({
 | 
			
		||||
            data: {
 | 
			
		||||
              identifier,
 | 
			
		||||
              token: hashToken(token),
 | 
			
		||||
              expires
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          await sendVerificationRequest({
 | 
			
		||||
            identifier,
 | 
			
		||||
            url,
 | 
			
		||||
            token,
 | 
			
		||||
            baseUrl: appOptions.baseUrl,
 | 
			
		||||
            provider
 | 
			
		||||
          })
 | 
			
		||||
          return verificationRequest
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async getVerificationRequest (identifier, token) {
 | 
			
		||||
          const hashedToken = hashToken(token)
 | 
			
		||||
          const verificationRequest = await prisma[VerificationRequest].findFirst({
 | 
			
		||||
            where: {
 | 
			
		||||
              identifier,
 | 
			
		||||
              token: hashedToken
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
 | 
			
		||||
            await prisma[VerificationRequest].deleteMany({
 | 
			
		||||
              where: {
 | 
			
		||||
                identifier,
 | 
			
		||||
                token: hashedToken
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
            return null
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return verificationRequest
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async deleteVerificationRequest (identifier, token) {
 | 
			
		||||
          await prisma[VerificationRequest].deleteMany({
 | 
			
		||||
            where: {
 | 
			
		||||
              identifier,
 | 
			
		||||
              token: hashToken(token)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -40,12 +40,6 @@ module.exports = withPlausibleProxy()({
 | 
			
		||||
        source: '/_next/:asset*',
 | 
			
		||||
        headers: corsHeaders
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        source: '/dark.js',
 | 
			
		||||
        headers: [
 | 
			
		||||
          ...corsHeaders
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        source: '/.well-known/:slug*',
 | 
			
		||||
        headers: [
 | 
			
		||||
@ -120,6 +114,10 @@ module.exports = withPlausibleProxy()({
 | 
			
		||||
        source: '/.well-known/web-app-origin-association',
 | 
			
		||||
        destination: '/api/web-app-origin-association'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        source: '/~:sub/:slug*\\?:query*',
 | 
			
		||||
        destination: '/~/:slug*?:query*&sub=:sub'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        source: '/~:sub/:slug*',
 | 
			
		||||
        destination: '/~/:slug*?sub=:sub'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										520
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										520
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
								
							@ -6,11 +6,11 @@
 | 
			
		||||
    "dev": "NODE_OPTIONS='--trace-warnings' next dev",
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "migrate": "prisma migrate deploy",
 | 
			
		||||
    "start": "NODE_OPTIONS='--trace-warnings' next start -p $PORT"
 | 
			
		||||
    "start": "NODE_OPTIONS='--trace-warnings' next start -p $PORT --keepAliveTimeout 120000"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@apollo/client": "^3.7.17",
 | 
			
		||||
    "@apollo/server": "^4.8.1",
 | 
			
		||||
    "@apollo/server": "^4.9.0",
 | 
			
		||||
    "@as-integrations/next": "^2.0.1",
 | 
			
		||||
    "@auth/prisma-adapter": "^1.0.1",
 | 
			
		||||
    "@graphql-tools/schema": "^10.0.0",
 | 
			
		||||
@ -22,12 +22,11 @@
 | 
			
		||||
    "acorn": "^8.10.0",
 | 
			
		||||
    "ajv": "^8.12.0",
 | 
			
		||||
    "async-retry": "^1.3.1",
 | 
			
		||||
    "aws-sdk": "^2.1422.0",
 | 
			
		||||
    "aws-sdk": "^2.1425.0",
 | 
			
		||||
    "babel-plugin-inline-react-svg": "^2.0.2",
 | 
			
		||||
    "bech32": "^2.0.0",
 | 
			
		||||
    "bolt11": "^1.4.1",
 | 
			
		||||
    "bootstrap": "^5.3.0",
 | 
			
		||||
    "browserslist": "^4.21.4",
 | 
			
		||||
    "bootstrap": "^5.3.1",
 | 
			
		||||
    "canonical-json": "0.0.4",
 | 
			
		||||
    "clipboard-copy": "^4.0.1",
 | 
			
		||||
    "cross-fetch": "^4.0.0",
 | 
			
		||||
@ -45,18 +44,19 @@
 | 
			
		||||
    "mdast-util-gfm": "^3.0.0",
 | 
			
		||||
    "mdast-util-to-string": "^4.0.0",
 | 
			
		||||
    "micromark-extension-gfm": "^3.0.0",
 | 
			
		||||
    "next": "^13.4.12",
 | 
			
		||||
    "next": "^13.4.13-canary.12",
 | 
			
		||||
    "next-auth": "^4.22.3",
 | 
			
		||||
    "next-plausible": "^3.10.1",
 | 
			
		||||
    "next-seo": "^6.1.0",
 | 
			
		||||
    "nextjs-progressbar": "0.0.16",
 | 
			
		||||
    "node-s3-url-encode": "^0.0.4",
 | 
			
		||||
    "nodemailer": "^6.9.4",
 | 
			
		||||
    "nostr": "^0.2.8",
 | 
			
		||||
    "nprogress": "^0.2.0",
 | 
			
		||||
    "opentimestamps": "^0.4.9",
 | 
			
		||||
    "page-metadata-parser": "^1.1.4",
 | 
			
		||||
    "pageres": "^7.1.0",
 | 
			
		||||
    "pg-boss": "^9.0.3",
 | 
			
		||||
    "prisma": "^5.0.0",
 | 
			
		||||
    "qrcode.react": "^3.1.0",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-avatar-editor": "^13.0.0",
 | 
			
		||||
@ -75,11 +75,11 @@
 | 
			
		||||
    "remark-gfm": "^3.0.1",
 | 
			
		||||
    "remove-markdown": "^0.5.0",
 | 
			
		||||
    "sass": "^1.64.1",
 | 
			
		||||
    "tldts": "^6.0.12",
 | 
			
		||||
    "tldts": "^6.0.13",
 | 
			
		||||
    "typescript": "^5.1.6",
 | 
			
		||||
    "unist-util-visit": "^5.0.0",
 | 
			
		||||
    "url-unshort": "^6.1.0",
 | 
			
		||||
    "web-push": "^3.6.2",
 | 
			
		||||
    "web-push": "^3.6.4",
 | 
			
		||||
    "webln": "^0.3.2",
 | 
			
		||||
    "webpack": "^5.88.2",
 | 
			
		||||
    "workbox-navigation-preload": "^7.0.0",
 | 
			
		||||
@ -92,7 +92,7 @@
 | 
			
		||||
    "yup": "^1.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": "18.16.1"
 | 
			
		||||
    "node": "18.17.0"
 | 
			
		||||
  },
 | 
			
		||||
  "standard": {
 | 
			
		||||
    "parser": "@babel/eslint-parser",
 | 
			
		||||
@ -107,8 +107,7 @@
 | 
			
		||||
    "@babel/core": "^7.22.9",
 | 
			
		||||
    "@babel/eslint-parser": "^7.22.9",
 | 
			
		||||
    "@next/eslint-plugin-next": "^13.4.12",
 | 
			
		||||
    "eslint": "^8.45.0",
 | 
			
		||||
    "prisma": "^5.0.0",
 | 
			
		||||
    "eslint": "^8.46.0",
 | 
			
		||||
    "standard": "^17.1.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import { ApolloProvider, gql } from '@apollo/client'
 | 
			
		||||
import { MeProvider } from '../components/me'
 | 
			
		||||
import PlausibleProvider from 'next-plausible'
 | 
			
		||||
import getApolloClient from '../lib/apollo'
 | 
			
		||||
import NextNProgress from 'nextjs-progressbar'
 | 
			
		||||
import { PriceProvider } from '../components/price'
 | 
			
		||||
import Head from 'next/head'
 | 
			
		||||
import { useRouter } from 'next/dist/client/router'
 | 
			
		||||
@ -12,8 +11,13 @@ import { ShowModalProvider } from '../components/modal'
 | 
			
		||||
import ErrorBoundary from '../components/error-boundary'
 | 
			
		||||
import { LightningProvider } from '../components/lightning'
 | 
			
		||||
import { ServiceWorkerProvider } from '../components/serviceworker'
 | 
			
		||||
import { SSR } from '../lib/constants'
 | 
			
		||||
import NProgress from 'nprogress'
 | 
			
		||||
import 'nprogress/nprogress.css'
 | 
			
		||||
 | 
			
		||||
const SSR = typeof window === 'undefined'
 | 
			
		||||
NProgress.configure({
 | 
			
		||||
  showSpinner: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function writeQuery (client, apollo, data) {
 | 
			
		||||
  if (apollo && data) {
 | 
			
		||||
@ -21,8 +25,8 @@ function writeQuery (client, apollo, data) {
 | 
			
		||||
      query: gql`${apollo.query}`,
 | 
			
		||||
      data,
 | 
			
		||||
      variables: apollo.variables,
 | 
			
		||||
      broadcast: !SSR,
 | 
			
		||||
      overwrite: SSR
 | 
			
		||||
      overwrite: SSR,
 | 
			
		||||
      broadcast: false
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -32,48 +36,49 @@ function MyApp ({ Component, pageProps: { ...props } }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const nprogressStart = (_, { shallow }) => !shallow && NProgress.start()
 | 
			
		||||
    const nprogressDone = (_, { shallow }) => !shallow && NProgress.done()
 | 
			
		||||
 | 
			
		||||
    router.events.on('routeChangeStart', nprogressStart)
 | 
			
		||||
    router.events.on('routeChangeComplete', nprogressDone)
 | 
			
		||||
    router.events.on('routeChangeError', nprogressDone)
 | 
			
		||||
 | 
			
		||||
    if (!props?.apollo) return
 | 
			
		||||
    // HACK: 'cause there's no way to tell Next to skip SSR
 | 
			
		||||
    // So every page load, we modify the route in browser history
 | 
			
		||||
    // to point to the same page but without SSR, ie ?nodata=true
 | 
			
		||||
    // this nodata var will get passed to the server on back/foward and
 | 
			
		||||
    // 1. prevent data from reloading and 2. perserve scroll
 | 
			
		||||
    // (2) is not possible while intercepting nav with beforePopState
 | 
			
		||||
    if (router.query.nodata) return
 | 
			
		||||
 | 
			
		||||
    router.replace({
 | 
			
		||||
      pathname: router.pathname,
 | 
			
		||||
      query: { ...router.query, nodata: true }
 | 
			
		||||
    }, router.asPath, { ...router.options, shallow: true }).catch((e) => {
 | 
			
		||||
      // workaround for https://github.com/vercel/next.js/issues/37362
 | 
			
		||||
      if (!e.cancelled) {
 | 
			
		||||
        console.log(e)
 | 
			
		||||
        throw e
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }, [router.pathname, router.query])
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      router.events.off('routeChangeStart', nprogressStart)
 | 
			
		||||
      router.events.off('routeChangeComplete', nprogressDone)
 | 
			
		||||
      router.events.off('routeChangeError', nprogressDone)
 | 
			
		||||
    }
 | 
			
		||||
  }, [router.asPath, props?.apollo])
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
    If we are on the client, we populate the apollo cache with the
 | 
			
		||||
    ssr data
 | 
			
		||||
  */
 | 
			
		||||
  const { apollo, ssrData, me, price, ...otherProps } = props
 | 
			
		||||
  // if we are on the server, useEffect won't run
 | 
			
		||||
  if (SSR && client) {
 | 
			
		||||
    writeQuery(client, apollo, ssrData)
 | 
			
		||||
  }
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    writeQuery(client, apollo, ssrData)
 | 
			
		||||
  }, [client, apollo, ssrData])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NextNProgress
 | 
			
		||||
        color='var(--bs-primary)'
 | 
			
		||||
        startPosition={0.3}
 | 
			
		||||
        stopDelayMs={200}
 | 
			
		||||
        height={2}
 | 
			
		||||
        showOnShallow={false}
 | 
			
		||||
        options={{ showSpinner: false }}
 | 
			
		||||
      />
 | 
			
		||||
      <Head>
 | 
			
		||||
        <meta name='viewport' content='initial-scale=1.0, width=device-width' />
 | 
			
		||||
      </Head>
 | 
			
		||||
 | 
			
		||||
@ -23,8 +23,43 @@ class MyDocument extends Document {
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <meta name='apple-mobile-web-app-capable' content='yes' />
 | 
			
		||||
          <meta name='theme-color' content='#000000' />
 | 
			
		||||
          <meta name='theme-color' content='#121214' />
 | 
			
		||||
          <link rel='apple-touch-icon' href='/icons/icon_x192.png' />
 | 
			
		||||
          <Script id='dark-mode-js' strategy='beforeInteractive'>
 | 
			
		||||
            {`const handleThemeChange = (dark) => {
 | 
			
		||||
                const root = window.document.documentElement
 | 
			
		||||
                root.setAttribute('data-bs-theme', dark ? 'dark' : 'light')
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              const STORAGE_KEY = 'darkMode'
 | 
			
		||||
              const PREFER_DARK_QUERY = '(prefers-color-scheme: dark)'
 | 
			
		||||
 | 
			
		||||
              const getTheme = () => {
 | 
			
		||||
                const mql = window.matchMedia(PREFER_DARK_QUERY)
 | 
			
		||||
                const supportsColorSchemeQuery = mql.media === PREFER_DARK_QUERY
 | 
			
		||||
                let localStorageTheme = null
 | 
			
		||||
                try {
 | 
			
		||||
                  localStorageTheme = window.localStorage.getItem(STORAGE_KEY)
 | 
			
		||||
                } catch (err) {}
 | 
			
		||||
                const localStorageExists = localStorageTheme !== null
 | 
			
		||||
                if (localStorageExists) {
 | 
			
		||||
                  localStorageTheme = JSON.parse(localStorageTheme)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (localStorageExists) {
 | 
			
		||||
                  return { user: true, dark: localStorageTheme }
 | 
			
		||||
                } else if (supportsColorSchemeQuery) {
 | 
			
		||||
                  return { user: false, dark: mql.matches }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (typeof window !== 'undefined') {
 | 
			
		||||
                (function () {
 | 
			
		||||
                  const { dark } = getTheme()
 | 
			
		||||
                  handleThemeChange(dark)
 | 
			
		||||
                })()
 | 
			
		||||
              }`}
 | 
			
		||||
          </Script>
 | 
			
		||||
          <link rel='apple-touch-startup-image' media='screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Pro_Max_landscape.png' />
 | 
			
		||||
          <link rel='apple-touch-startup-image' media='screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Pro_landscape.png' />
 | 
			
		||||
          <link rel='apple-touch-startup-image' media='screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' href='/splash/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png' />
 | 
			
		||||
@ -59,7 +94,6 @@ class MyDocument extends Document {
 | 
			
		||||
          <link rel='apple-touch-startup-image' media='screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/10.2__iPad_portrait.png' />
 | 
			
		||||
          <link rel='apple-touch-startup-image' media='screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png' />
 | 
			
		||||
          <link rel='apple-touch-startup-image' media='screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' href='/splash/8.3__iPad_Mini_portrait.png' />
 | 
			
		||||
          <Script src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/dark.js`} crossOrigin='' strategy='beforeInteractive' type='module' />
 | 
			
		||||
        </Head>
 | 
			
		||||
        <body>
 | 
			
		||||
          <Main />
 | 
			
		||||
 | 
			
		||||
@ -130,7 +130,7 @@ const providers = [
 | 
			
		||||
    },
 | 
			
		||||
    profile: profile => {
 | 
			
		||||
      return {
 | 
			
		||||
        ...profile,
 | 
			
		||||
        id: profile.id,
 | 
			
		||||
        name: profile.login
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -140,7 +140,7 @@ const providers = [
 | 
			
		||||
    clientSecret: process.env.TWITTER_SECRET,
 | 
			
		||||
    profile: profile => {
 | 
			
		||||
      return {
 | 
			
		||||
        ...profile,
 | 
			
		||||
        id: profile.id,
 | 
			
		||||
        name: profile.screen_name
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ const apolloServer = new ApolloServer({
 | 
			
		||||
              return (error, result) => {
 | 
			
		||||
                const end = process.hrtime.bigint()
 | 
			
		||||
                const ms = (end - start) / 1000000n
 | 
			
		||||
                if (ms > 5) {
 | 
			
		||||
                if (ms > 50) {
 | 
			
		||||
                  console.log(`Field ${info.parentType.name}.${info.fieldName} took ${ms}ms`)
 | 
			
		||||
                }
 | 
			
		||||
                if (error) {
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ export default async ({ query: { username, amount, nostr } }, res) => {
 | 
			
		||||
 | 
			
		||||
    await serialize(models,
 | 
			
		||||
      models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
 | 
			
		||||
        ${expiresAt}, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description})`)
 | 
			
		||||
        ${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description})`)
 | 
			
		||||
 | 
			
		||||
    return res.status(200).json({
 | 
			
		||||
      pr: invoice.request,
 | 
			
		||||
 | 
			
		||||
@ -8,10 +8,10 @@ import { INVITE_FIELDS } from '../../fragments/invites'
 | 
			
		||||
import getSSRApolloClient from '../../api/ssrApollo'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { CenterLayout } from '../../components/layout'
 | 
			
		||||
import { authOptions } from '../api/auth/[...nextauth]'
 | 
			
		||||
import { getAuthOptions } from '../api/auth/[...nextauth]'
 | 
			
		||||
 | 
			
		||||
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
 | 
			
		||||
  const session = await getServerSession(req, res, authOptions(req))
 | 
			
		||||
  const session = await getServerSession(req, res, getAuthOptions(req))
 | 
			
		||||
 | 
			
		||||
  const client = await getSSRApolloClient({ req, res })
 | 
			
		||||
  const { data } = await client.query({
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import AccordianItem from '../../components/accordian-item'
 | 
			
		||||
import styles from '../../styles/invites.module.css'
 | 
			
		||||
import Invite from '../../components/invite'
 | 
			
		||||
import { inviteSchema } from '../../lib/validate'
 | 
			
		||||
import { SSR } from '../../lib/constants'
 | 
			
		||||
 | 
			
		||||
function InviteForm () {
 | 
			
		||||
  const [createInvite] = useMutation(
 | 
			
		||||
@ -93,7 +94,7 @@ export default function Invites () {
 | 
			
		||||
          ...InviteFields
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    `, { fetchPolicy: 'cache-and-network' })
 | 
			
		||||
    `, SSR ? {} : { fetchPolicy: 'cache-and-network' })
 | 
			
		||||
 | 
			
		||||
  const [active, inactive] = data && data.invites
 | 
			
		||||
    ? data.invites.reduce((result, invite) => {
 | 
			
		||||
 | 
			
		||||
@ -4,14 +4,17 @@ import { QrSkeleton } from '../../components/qr'
 | 
			
		||||
import { CenterLayout } from '../../components/layout'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { INVOICE } from '../../fragments/wallet'
 | 
			
		||||
import { SSR } from '../../lib/constants'
 | 
			
		||||
 | 
			
		||||
export default function FullInvoice ({ id }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const { data, error } = useQuery(INVOICE, {
 | 
			
		||||
    pollInterval: 1000,
 | 
			
		||||
    variables: { id: router.query.id },
 | 
			
		||||
    nextFetchPolicy: 'cache-and-network'
 | 
			
		||||
  })
 | 
			
		||||
  const { data, error } = useQuery(INVOICE, SSR
 | 
			
		||||
    ? {}
 | 
			
		||||
    : {
 | 
			
		||||
        pollInterval: 1000,
 | 
			
		||||
        variables: { id: router.query.id },
 | 
			
		||||
        nextFetchPolicy: 'cache-and-network'
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CenterLayout>
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ function Ots ({ item }) {
 | 
			
		||||
        : (
 | 
			
		||||
          <pre
 | 
			
		||||
            className='mb-2 p-2 rounded'
 | 
			
		||||
            style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', border: '1px solid var(--theme-borderColor)', color: 'var(--theme-color)' }}
 | 
			
		||||
            style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', border: '1px solid var(--theme-borderColor)', color: 'var(--bs-body-color)' }}
 | 
			
		||||
          >{itemString}
 | 
			
		||||
          </pre>)}
 | 
			
		||||
      <Button href={`/api/ots/preimage/${item.id}`} className='mt-1' variant='grey-medium'>download preimage</Button>
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,27 @@
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import { getGetServerSideProps } from '../api/ssrApollo'
 | 
			
		||||
import Layout from '../components/layout'
 | 
			
		||||
import Notifications from '../components/notifications'
 | 
			
		||||
import { NOTIFICATIONS } from '../fragments/notifications'
 | 
			
		||||
import Notifications, { NotificationAlert } from '../components/notifications'
 | 
			
		||||
import { HAS_NOTIFICATIONS, NOTIFICATIONS } from '../fragments/notifications'
 | 
			
		||||
import { useApolloClient } from '@apollo/client'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(NOTIFICATIONS)
 | 
			
		||||
 | 
			
		||||
export default function NotificationPage ({ ssrData }) {
 | 
			
		||||
  const client = useApolloClient()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    client?.writeQuery({
 | 
			
		||||
      query: HAS_NOTIFICATIONS,
 | 
			
		||||
      data: {
 | 
			
		||||
        hasNewNotes: false
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <NotificationAlert />
 | 
			
		||||
      <Notifications ssrData={ssrData} />
 | 
			
		||||
    </Layout>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { gql } from 'graphql-tag'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { useMemo } from 'react'
 | 
			
		||||
import Button from 'react-bootstrap/Button'
 | 
			
		||||
import InputGroup from 'react-bootstrap/InputGroup'
 | 
			
		||||
import { getGetServerSideProps } from '../api/ssrApollo'
 | 
			
		||||
@ -13,6 +13,7 @@ import { abbrNum } from '../lib/format'
 | 
			
		||||
import PageLoading from '../components/page-loading'
 | 
			
		||||
import { useShowModal } from '../components/modal'
 | 
			
		||||
import dynamic from 'next/dynamic'
 | 
			
		||||
import { SSR } from '../lib/constants'
 | 
			
		||||
 | 
			
		||||
const GrowthPieChart = dynamic(() => import('../components/charts').then(mod => mod.GrowthPieChart), {
 | 
			
		||||
  loading: () => <div>Loading...</div>
 | 
			
		||||
@ -47,11 +48,7 @@ function midnight (tz) {
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(REWARDS)
 | 
			
		||||
 | 
			
		||||
export function RewardLine ({ total }) {
 | 
			
		||||
  const [threshold, setThreshold] = useState(0)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setThreshold(midnight('America/Chicago'))
 | 
			
		||||
  }, [])
 | 
			
		||||
  const threshold = useMemo(() => midnight('America/Chicago'))
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
@ -59,14 +56,14 @@ export function RewardLine ({ total }) {
 | 
			
		||||
      {threshold &&
 | 
			
		||||
        <Countdown
 | 
			
		||||
          date={threshold}
 | 
			
		||||
          renderer={props => <small className='text-monospace'> {props.formatted.hours}:{props.formatted.minutes}:{props.formatted.seconds}</small>}
 | 
			
		||||
          renderer={props => <small className='text-monospace' suppressHydrationWarning> {props.formatted.hours}:{props.formatted.minutes}:{props.formatted.seconds}</small>}
 | 
			
		||||
        />}
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Rewards ({ ssrData }) {
 | 
			
		||||
  const { data } = useQuery(REWARDS, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  const { data } = useQuery(REWARDS, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  if (!data && !ssrData) return <PageLoading />
 | 
			
		||||
 | 
			
		||||
  const { expectedRewards: { total, sources } } = data || ssrData
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ function satusClass (status) {
 | 
			
		||||
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case 'CONFIRMED':
 | 
			
		||||
      return ''
 | 
			
		||||
      return 'text-reset'
 | 
			
		||||
    case 'PENDING':
 | 
			
		||||
      return 'text-muted'
 | 
			
		||||
    default:
 | 
			
		||||
@ -78,9 +78,9 @@ function Satus ({ status }) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
    <span className='d-block'>
 | 
			
		||||
      <Icon /><small className={`text-${color} fw-bold ms-2`}>{desc}</small>
 | 
			
		||||
    </div>
 | 
			
		||||
    </span>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -110,10 +110,10 @@ function Detail ({ fact }) {
 | 
			
		||||
  if (!fact.item) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='px-3'>
 | 
			
		||||
        <div className={satusClass(fact.status)}>
 | 
			
		||||
        <Link className={satusClass(fact.status)} href={`/${fact.type}s/${fact.factId}`}>
 | 
			
		||||
          {fact.description || 'no invoice description'}
 | 
			
		||||
        </div>
 | 
			
		||||
        <Satus status={fact.status} />
 | 
			
		||||
          <Satus status={fact.status} />
 | 
			
		||||
        </Link>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
@ -206,7 +206,7 @@ export default function Satistics ({ ssrData }) {
 | 
			
		||||
            <div className={[styles.type, styles.head].join(' ')}>type</div>
 | 
			
		||||
            <div className={[styles.detail, styles.head].join(' ')}>detail</div>
 | 
			
		||||
            <div className={[styles.sats, styles.head].join(' ')}>sats</div>
 | 
			
		||||
            {facts.map(f => <Fact key={f.factId} fact={f} />)}
 | 
			
		||||
            {facts.map(f => <Fact key={f.id} fact={f} />)}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <MoreFooter cursor={cursor} count={facts?.length} fetchMore={fetchMore} Skeleton={PageLoading} />
 | 
			
		||||
 | 
			
		||||
@ -245,7 +245,7 @@ export default function Settings ({ ssrData }) {
 | 
			
		||||
            name='greeterMode'
 | 
			
		||||
          />
 | 
			
		||||
          <AccordianItem
 | 
			
		||||
            headerColor='var(--theme-color)'
 | 
			
		||||
            headerColor='var(--bs-body-color)'
 | 
			
		||||
            show={settings?.nostrPubkey}
 | 
			
		||||
            header={<h4 className='text-left'>nostr <small><a href='https://github.com/nostr-protocol/nips/blob/master/05.md' target='_blank' rel='noreferrer'>NIP-05</a></small></h4>}
 | 
			
		||||
            body={
 | 
			
		||||
@ -372,14 +372,19 @@ function AuthMethods ({ methods }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className='form-label mt-3'>auth methods</div>
 | 
			
		||||
      {err && <Alert variant='danger' onClose={() => {
 | 
			
		||||
        const { pathname, query: { error, nodata, ...rest } } = router
 | 
			
		||||
        router.replace({
 | 
			
		||||
          pathname,
 | 
			
		||||
          query: { nodata, ...rest }
 | 
			
		||||
        }, { pathname, query: { ...rest } }, { shallow: true })
 | 
			
		||||
        setErr(undefined)
 | 
			
		||||
      }} dismissible>{err}</Alert>}
 | 
			
		||||
      {err && (
 | 
			
		||||
        <Alert
 | 
			
		||||
          variant='danger' onClose={() => {
 | 
			
		||||
            const { pathname, query: { error, nodata, ...rest } } = router
 | 
			
		||||
            router.replace({
 | 
			
		||||
              pathname,
 | 
			
		||||
              query: { nodata, ...rest }
 | 
			
		||||
            }, { pathname, query: { ...rest } }, { shallow: true })
 | 
			
		||||
            setErr(undefined)
 | 
			
		||||
          }} dismissible
 | 
			
		||||
        >{err}
 | 
			
		||||
        </Alert>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {providers?.map(provider => {
 | 
			
		||||
        if (provider === 'email') {
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ import Alert from 'react-bootstrap/Alert'
 | 
			
		||||
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
 | 
			
		||||
import { getGetServerSideProps } from '../api/ssrApollo'
 | 
			
		||||
import { amountSchema, lnAddrSchema, withdrawlSchema } from '../lib/validate'
 | 
			
		||||
import { SSR } from '../lib/constants'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps()
 | 
			
		||||
 | 
			
		||||
@ -210,7 +211,7 @@ function LnQRWith ({ k1, encodedUrl }) {
 | 
			
		||||
      k1
 | 
			
		||||
    }
 | 
			
		||||
  }`
 | 
			
		||||
  const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
  const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
 | 
			
		||||
 | 
			
		||||
  if (data?.lnWith?.withdrawalId) {
 | 
			
		||||
    router.push(`/withdrawals/${data.lnWith.withdrawalId}`)
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import InvoiceStatus from '../../components/invoice-status'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { WITHDRAWL } from '../../fragments/wallet'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { SSR } from '../../lib/constants'
 | 
			
		||||
 | 
			
		||||
export default function Withdrawl () {
 | 
			
		||||
  return (
 | 
			
		||||
@ -31,11 +32,13 @@ export function WithdrawlSkeleton ({ status }) {
 | 
			
		||||
 | 
			
		||||
function LoadWithdrawl () {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const { loading, error, data } = useQuery(WITHDRAWL, {
 | 
			
		||||
    variables: { id: router.query.id },
 | 
			
		||||
    pollInterval: 1000,
 | 
			
		||||
    nextFetchPolicy: 'cache-and-network'
 | 
			
		||||
  })
 | 
			
		||||
  const { loading, error, data } = useQuery(WITHDRAWL, SSR
 | 
			
		||||
    ? {}
 | 
			
		||||
    : {
 | 
			
		||||
        variables: { id: router.query.id },
 | 
			
		||||
        pollInterval: 1000,
 | 
			
		||||
        nextFetchPolicy: 'cache-and-network'
 | 
			
		||||
      })
 | 
			
		||||
  if (error) return <div>error</div>
 | 
			
		||||
  if (!data || loading) {
 | 
			
		||||
    return <WithdrawlSkeleton status='loading' />
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								prisma/migrations/20230731135152_timezone_utc/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								prisma/migrations/20230731135152_timezone_utc/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
-- prod is set to utc by default but dev might not be
 | 
			
		||||
CREATE OR REPLACE FUNCTION set_timezone_utc_currentdb()
 | 
			
		||||
RETURNS INTEGER
 | 
			
		||||
LANGUAGE plpgsql
 | 
			
		||||
AS $$
 | 
			
		||||
DECLARE
 | 
			
		||||
BEGIN
 | 
			
		||||
    EXECUTE 'ALTER DATABASE '||current_database()||' SET TIMEZONE TO ''UTC''';
 | 
			
		||||
    return 0;
 | 
			
		||||
EXCEPTION WHEN OTHERS THEN
 | 
			
		||||
    return 0;
 | 
			
		||||
END;
 | 
			
		||||
$$;
 | 
			
		||||
 | 
			
		||||
SELECT set_timezone_utc_currentdb();
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
-- AlterTable
 | 
			
		||||
ALTER TABLE "accounts" ADD COLUMN     "oauth_token" TEXT,
 | 
			
		||||
ADD COLUMN     "oauth_token_secret" TEXT;
 | 
			
		||||
@ -443,6 +443,10 @@ model Account {
 | 
			
		||||
  id_token          String?
 | 
			
		||||
  session_state     String?
 | 
			
		||||
 | 
			
		||||
  // twitter oauth 1.0 needs these https://authjs.dev/reference/core/providers_twitter#notes
 | 
			
		||||
  oauth_token        String?
 | 
			
		||||
  oauth_token_secret String?
 | 
			
		||||
 | 
			
		||||
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
			
		||||
 | 
			
		||||
  @@unique([provider, providerAccountId])
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								public/dark-mode.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								public/dark-mode.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
const handleThemeChange = (dark) => {
 | 
			
		||||
  const root = window.document.documentElement
 | 
			
		||||
  root.setAttribute('data-bs-theme', dark ? 'dark' : 'light')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const STORAGE_KEY = 'darkMode'
 | 
			
		||||
const PREFER_DARK_QUERY = '(prefers-color-scheme: dark)'
 | 
			
		||||
 | 
			
		||||
const getTheme = () => {
 | 
			
		||||
  const mql = window.matchMedia(PREFER_DARK_QUERY)
 | 
			
		||||
  const supportsColorSchemeQuery = mql.media === PREFER_DARK_QUERY
 | 
			
		||||
  let localStorageTheme = null
 | 
			
		||||
  try {
 | 
			
		||||
    localStorageTheme = window.localStorage.getItem(STORAGE_KEY)
 | 
			
		||||
  } catch (err) {}
 | 
			
		||||
  const localStorageExists = localStorageTheme !== null
 | 
			
		||||
  if (localStorageExists) {
 | 
			
		||||
    localStorageTheme = JSON.parse(localStorageTheme)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (localStorageExists) {
 | 
			
		||||
    return { user: true, dark: localStorageTheme }
 | 
			
		||||
  } else if (supportsColorSchemeQuery) {
 | 
			
		||||
    return { user: false, dark: mql.matches }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (typeof window !== 'undefined') {
 | 
			
		||||
  (function () {
 | 
			
		||||
    const { dark } = getTheme()
 | 
			
		||||
    handleThemeChange(dark)
 | 
			
		||||
  })()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								public/dark.js
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								public/dark.js
									
									
									
									
									
								
							@ -1,112 +0,0 @@
 | 
			
		||||
const COLORS = {
 | 
			
		||||
  light: {
 | 
			
		||||
    body: '#f5f5f7',
 | 
			
		||||
    color: '#212529',
 | 
			
		||||
    navbarVariant: 'light',
 | 
			
		||||
    navLink: 'rgba(0, 0, 0, 0.55)',
 | 
			
		||||
    navLinkFocus: 'rgba(0, 0, 0, 0.7)',
 | 
			
		||||
    navLinkActive: 'rgba(0, 0, 0, 0.9)',
 | 
			
		||||
    borderColor: '#ced4da',
 | 
			
		||||
    inputBg: '#ffffff',
 | 
			
		||||
    inputDisabledBg: '#e9ecef',
 | 
			
		||||
    dropdownItemColor: 'rgba(0, 0, 0, 0.7)',
 | 
			
		||||
    dropdownItemColorHover: 'rgba(0, 0, 0, 0.9)',
 | 
			
		||||
    commentBg: 'rgba(0, 0, 0, 0.03)',
 | 
			
		||||
    clickToContextColor: 'rgba(0, 0, 0, 0.07)',
 | 
			
		||||
    brandColor: 'rgba(0, 0, 0, 0.9)',
 | 
			
		||||
    grey: '#707070',
 | 
			
		||||
    link: '#007cbe',
 | 
			
		||||
    toolbarActive: 'rgba(0, 0, 0, 0.10)',
 | 
			
		||||
    toolbarHover: 'rgba(0, 0, 0, 0.20)',
 | 
			
		||||
    toolbar: '#ffffff',
 | 
			
		||||
    quoteBar: 'rgb(206, 208, 212)',
 | 
			
		||||
    quoteColor: 'rgb(101, 103, 107)',
 | 
			
		||||
    linkHover: '#004a72',
 | 
			
		||||
    linkVisited: '#537587'
 | 
			
		||||
  },
 | 
			
		||||
  dark: {
 | 
			
		||||
    body: '#000000',
 | 
			
		||||
    inputBg: '#000000',
 | 
			
		||||
    inputDisabledBg: '#000000',
 | 
			
		||||
    navLink: 'rgba(255, 255, 255, 0.55)',
 | 
			
		||||
    navLinkFocus: 'rgba(255, 255, 255, 0.75)',
 | 
			
		||||
    navLinkActive: 'rgba(255, 255, 255, 0.9)',
 | 
			
		||||
    borderColor: 'rgba(255, 255, 255, 0.5)',
 | 
			
		||||
    dropdownItemColor: 'rgba(255, 255, 255, 0.7)',
 | 
			
		||||
    dropdownItemColorHover: 'rgba(255, 255, 255, 0.9)',
 | 
			
		||||
    commentBg: 'rgba(255, 255, 255, 0.04)',
 | 
			
		||||
    clickToContextColor: 'rgba(255, 255, 255, 0.2)',
 | 
			
		||||
    color: '#f8f9fa',
 | 
			
		||||
    brandColor: 'var(--bs-primary)',
 | 
			
		||||
    grey: '#969696',
 | 
			
		||||
    link: '#2e99d1',
 | 
			
		||||
    toolbarActive: 'rgba(255, 255, 255, 0.10)',
 | 
			
		||||
    toolbarHover: 'rgba(255, 255, 255, 0.20)',
 | 
			
		||||
    toolbar: '#3e3f3f',
 | 
			
		||||
    quoteBar: 'rgb(158, 159, 163)',
 | 
			
		||||
    quoteColor: 'rgb(141, 144, 150)',
 | 
			
		||||
    linkHover: '#007cbe',
 | 
			
		||||
    linkVisited: '#56798E'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleThemeChange = (dark) => {
 | 
			
		||||
  const root = window.document.documentElement
 | 
			
		||||
  const colors = COLORS[dark ? 'dark' : 'light']
 | 
			
		||||
  Object.entries(colors).forEach(([varName, value]) => {
 | 
			
		||||
    const cssVarName = `--theme-${varName}`
 | 
			
		||||
    root.style.setProperty(cssVarName, value)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const STORAGE_KEY = 'darkMode'
 | 
			
		||||
const PREFER_DARK_QUERY = '(prefers-color-scheme: dark)'
 | 
			
		||||
 | 
			
		||||
export const getTheme = () => {
 | 
			
		||||
  const mql = window.matchMedia(PREFER_DARK_QUERY)
 | 
			
		||||
  const supportsColorSchemeQuery = mql.media === PREFER_DARK_QUERY
 | 
			
		||||
  let localStorageTheme = null
 | 
			
		||||
  try {
 | 
			
		||||
    localStorageTheme = window.localStorage.getItem(STORAGE_KEY)
 | 
			
		||||
  } catch (err) {}
 | 
			
		||||
  const localStorageExists = localStorageTheme !== null
 | 
			
		||||
  if (localStorageExists) {
 | 
			
		||||
    localStorageTheme = JSON.parse(localStorageTheme)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (localStorageExists) {
 | 
			
		||||
    return { user: true, dark: localStorageTheme }
 | 
			
		||||
  } else if (supportsColorSchemeQuery) {
 | 
			
		||||
    return { user: false, dark: mql.matches }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const setTheme = (dark) => {
 | 
			
		||||
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(dark))
 | 
			
		||||
  handleThemeChange(dark)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const listenForThemeChange = (onChange) => {
 | 
			
		||||
  const mql = window.matchMedia(PREFER_DARK_QUERY)
 | 
			
		||||
  mql.onchange = mql => {
 | 
			
		||||
    const { user, dark } = getTheme()
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      handleThemeChange(dark)
 | 
			
		||||
      onChange({ user, dark })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  window.onstorage = e => {
 | 
			
		||||
    if (e.key === STORAGE_KEY) {
 | 
			
		||||
      const dark = JSON.parse(e.newValue)
 | 
			
		||||
      setTheme(dark)
 | 
			
		||||
      onChange({ user: true, dark })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (typeof window !== 'undefined') {
 | 
			
		||||
  (function () {
 | 
			
		||||
    const { dark } = getTheme()
 | 
			
		||||
    handleThemeChange(dark)
 | 
			
		||||
  })()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/giphy.gif
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/giphy.gif
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 936 KiB  | 
@ -89,7 +89,7 @@
 | 
			
		||||
  ],
 | 
			
		||||
  "display": "standalone",
 | 
			
		||||
  "orientation": "any",
 | 
			
		||||
  "theme_color": "#000000",
 | 
			
		||||
  "theme_color": "#121214",
 | 
			
		||||
  "background_color": "#FADA5E",
 | 
			
		||||
  "id": "/",
 | 
			
		||||
  "start_url": "/",
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 14 KiB  | 
@ -23,7 +23,10 @@ $theme-colors: (
 | 
			
		||||
  "grey-darkmode": #8c8c8c,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
$body-bg: #f5f5f7;
 | 
			
		||||
$body-bg: #fcfcff;
 | 
			
		||||
$body-bg-dark: #121214;
 | 
			
		||||
$body-color: #212529;
 | 
			
		||||
$body-color-dark: #f0f0f0;
 | 
			
		||||
$border-radius: .4rem;
 | 
			
		||||
$enable-transitions: false;
 | 
			
		||||
$enable-gradients: false;
 | 
			
		||||
@ -41,6 +44,10 @@ $btn-font-weight: bold;
 | 
			
		||||
$btn-focus-width: 0;
 | 
			
		||||
$btn-border-width: 0;
 | 
			
		||||
$btn-focus-box-shadow: none;
 | 
			
		||||
$form-invalid-border-color: #c03221;
 | 
			
		||||
$form-invalid-border-color-dark: #c03221;
 | 
			
		||||
$form-invalid-color: #c03221;
 | 
			
		||||
$form-invalid-color-dark: #c03221;
 | 
			
		||||
$alert-border-width: 0;
 | 
			
		||||
$close-text-shadow: none;
 | 
			
		||||
$close-color: inherit;
 | 
			
		||||
@ -69,11 +76,12 @@ $nav-tabs-link-hover-border-color: transparent;
 | 
			
		||||
$nav-tabs-link-active-border-color: #ced4da #ced4da $nav-tabs-link-active-bg;
 | 
			
		||||
$form-check-input-checked-color: var(--bs-primary);
 | 
			
		||||
$form-check-input-checked-bg-color: var(--bs-primary);
 | 
			
		||||
$popover-bg: var(--theme-body);
 | 
			
		||||
$popover-bg: var(--bs-body-bg);
 | 
			
		||||
$form-check-input-checked-color: #000;
 | 
			
		||||
$tooltip-bg: #5c8001;
 | 
			
		||||
$form-select-indicator-color: #808080;
 | 
			
		||||
$form-select-indicator: url("data:image/svg+xml, %3Csvg fill='#{$form-select-indicator-color}' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath  d='M12 15.0006L7.75732 10.758L9.17154 9.34375L12 12.1722L14.8284 9.34375L16.2426 10.758L12 15.0006Z'%3E%3C/path%3E%3C/svg%3E%0A");
 | 
			
		||||
$form-select-indicator-dark: url("data:image/svg+xml, %3Csvg fill='#{$form-select-indicator-color}' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath  d='M12 15.0006L7.75732 10.758L9.17154 9.34375L12 12.1722L14.8284 9.34375L16.2426 10.758L12 15.0006Z'%3E%3C/path%3E%3C/svg%3E%0A");
 | 
			
		||||
$form-select-bg-position: right .25rem center;
 | 
			
		||||
$form-select-bg-size: 1.5rem;
 | 
			
		||||
$popover-body-padding-y: .5rem;
 | 
			
		||||
@ -81,6 +89,49 @@ $popover-max-width: 320px !default;
 | 
			
		||||
$popover-border-color: var(--theme-borderColor);
 | 
			
		||||
$grid-gutter-width: 2rem;
 | 
			
		||||
 | 
			
		||||
:root, [data-bs-theme=light] {
 | 
			
		||||
  --theme-navLink: rgba(0, 0, 0, 0.55);
 | 
			
		||||
  --theme-navLinkFocus: rgba(0, 0, 0, 0.7);
 | 
			
		||||
  --theme-navLinkActive: rgba(0, 0, 0, 0.9);
 | 
			
		||||
  --theme-borderColor: #ced4da;
 | 
			
		||||
  --theme-inputBg: #ffffff;
 | 
			
		||||
  --theme-inputDisabledBg: #e9ecef;
 | 
			
		||||
  --theme-dropdownItemColor: rgba(0, 0, 0, 0.7);
 | 
			
		||||
  --theme-dropdownItemColorHover: rgba(0, 0, 0, 0.9);
 | 
			
		||||
  --theme-commentBg: rgba(0, 0, 0, 0.03);
 | 
			
		||||
  --theme-clickToContextColor: rgba(0, 0, 0, 0.07);
 | 
			
		||||
  --theme-brandColor: rgba(0, 0, 0, 0.9);
 | 
			
		||||
  --theme-grey: #707070;
 | 
			
		||||
  --theme-link: #007cbe;
 | 
			
		||||
  --theme-quoteBar: rgb(206, 208, 212);
 | 
			
		||||
  --theme-linkHover: #004a72;
 | 
			
		||||
  --theme-linkVisited: #53758;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[data-bs-theme=dark] {
 | 
			
		||||
  color-scheme: dark;
 | 
			
		||||
  --theme-inputBg: #121211;
 | 
			
		||||
  --theme-inputDisabledBg: #121211;
 | 
			
		||||
  --theme-navLink: rgba(255, 255, 255, 0.55);
 | 
			
		||||
  --theme-navLinkFocus: rgba(255, 255, 255, 0.75);
 | 
			
		||||
  --theme-navLinkActive: rgba(255, 255, 255, 0.9);
 | 
			
		||||
  --theme-borderColor: rgba(255, 255, 255, 0.5);
 | 
			
		||||
  --theme-dropdownItemColor: rgba(255, 255, 255, 0.7);
 | 
			
		||||
  --theme-dropdownItemColorHover: rgba(255, 255, 255, 0.9);
 | 
			
		||||
  --theme-commentBg: rgba(255, 255, 255, 0.025);
 | 
			
		||||
  --theme-clickToContextColor: rgba(255, 255, 255, 0.1);
 | 
			
		||||
  --theme-brandColor: var(--bs-primary);
 | 
			
		||||
  --theme-grey: #969696;
 | 
			
		||||
  --theme-link: #2e99d1;
 | 
			
		||||
  --theme-toolbarActive: rgba(255, 255, 255, 0.10);
 | 
			
		||||
  --theme-toolbarHover: rgba(255, 255, 255, 0.20);
 | 
			
		||||
  --theme-toolbar: #3e3f3f;
 | 
			
		||||
  --theme-quoteBar: rgb(158, 159, 163);
 | 
			
		||||
  --theme-quoteColor: rgb(141, 144, 150);
 | 
			
		||||
  --theme-linkHover: #007cbe;
 | 
			
		||||
  --theme-linkVisited: #56798E;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@import '../node_modules/bootstrap/scss/bootstrap.scss';
 | 
			
		||||
 | 
			
		||||
@media screen and (min-width: 899px) {
 | 
			
		||||
@ -95,6 +146,15 @@ $grid-gutter-width: 2rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nprogress .bar {
 | 
			
		||||
  background: var(--bs-primary) !important;
 | 
			
		||||
  height: 2px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nprogress .peg {
 | 
			
		||||
  box-shadow: 0 0 10px var(--bs-primary), 0 0 5px var(--bs-primary) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.standalone {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
@ -145,7 +205,7 @@ dl {
 | 
			
		||||
mark {
 | 
			
		||||
  background-color: #fada5e5e;
 | 
			
		||||
  padding: 0 0.2rem;
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  color: var(--bs-body-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-sm th,
 | 
			
		||||
@ -172,8 +232,8 @@ mark {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  background-color: var(--theme-body);
 | 
			
		||||
  color: var(--bs-body-color);
 | 
			
		||||
  background-color: var(--bs-body-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table th,
 | 
			
		||||
@ -183,13 +243,13 @@ mark {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-hover tbody tr:hover {
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  color: var(--bs-body-color);
 | 
			
		||||
  background-color: var(--theme-clickToContextColor);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html, body {
 | 
			
		||||
  background: var(--theme-body);
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  background: var(--bs-body-bg) !important;
 | 
			
		||||
  color: var(--bs-body-color) !important;
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  min-height: 100svh;
 | 
			
		||||
}
 | 
			
		||||
@ -214,7 +274,7 @@ select,
 | 
			
		||||
div[contenteditable],
 | 
			
		||||
.form-control {
 | 
			
		||||
  background-color: var(--theme-inputBg);
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  color: var(--bs-body-color);
 | 
			
		||||
  border-color: var(--theme-borderColor);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -256,7 +316,7 @@ select:focus {
 | 
			
		||||
div[contenteditable]:focus,
 | 
			
		||||
.form-control:focus {
 | 
			
		||||
  background-color: var(--theme-inputBg);
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  color: var(--bs-body-color);
 | 
			
		||||
  outline: 0;
 | 
			
		||||
  box-shadow: 0 0 0 0.2rem rgb(250 218 94 / 25%);
 | 
			
		||||
}
 | 
			
		||||
@ -297,11 +357,6 @@ div[contenteditable]:disabled,
 | 
			
		||||
  fill: #212529;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fresh {
 | 
			
		||||
  background-color: var(--theme-clickToContextColor);
 | 
			
		||||
  border-radius: .4rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-content {
 | 
			
		||||
  background-color: var(--theme-inputBg);
 | 
			
		||||
  border-color: var(--theme-borderColor);
 | 
			
		||||
@ -406,7 +461,7 @@ footer {
 | 
			
		||||
.input-group-text {
 | 
			
		||||
  background-color: var(--theme-clickToContextColor);
 | 
			
		||||
  border-color: var(--theme-borderColor);
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  color: var(--bs-body-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea.form-control,
 | 
			
		||||
@ -445,8 +500,8 @@ div[contenteditable] {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.popover {
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  background-color: var(--theme-body);
 | 
			
		||||
  color: var(--bs-body-color);
 | 
			
		||||
  background-color: var(--bs-body-bg);
 | 
			
		||||
  border-color: var(--theme-borderColor);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -455,7 +510,7 @@ div[contenteditable] {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.popover .arrow::after {
 | 
			
		||||
  border-top-color: var(--theme-body);
 | 
			
		||||
  border-top-color: var(--bs-body-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -566,7 +621,7 @@ div[contenteditable]:focus,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fill-theme-color {
 | 
			
		||||
  fill: var(--theme-color);
 | 
			
		||||
  fill: var(--bs-body-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fill-warning {
 | 
			
		||||
@ -617,6 +672,34 @@ div[contenteditable]:focus,
 | 
			
		||||
  animation: flash 2s linear 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes outline {
 | 
			
		||||
  from {
 | 
			
		||||
    box-shadow: inset 0 0 1px 1px var(--bs-info);
 | 
			
		||||
  }
 | 
			
		||||
  90% {
 | 
			
		||||
    box-shadow: inset 0 0 1px 1px var(--bs-info);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    box-shadow: inset 0 0 0px 0px var(--bs-info);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.outline-it {
 | 
			
		||||
  animation: outline 3s linear 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.outline-new-comment {
 | 
			
		||||
  box-shadow: inset 0 0 1px 1px rgba(0, 123, 190, 0.25);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.outline-new-comment.outline-new-comment-unset {
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.outline-new-comment .outline-new-comment {
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: rotate(0deg);
 | 
			
		||||
@ -679,14 +762,15 @@ div[contenteditable]:focus,
 | 
			
		||||
 | 
			
		||||
.tooltip-inner {
 | 
			
		||||
  padding: 0.1rem 0.3rem;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.popover {
 | 
			
		||||
  .popover-header {
 | 
			
		||||
    background-color: var(--theme-body);
 | 
			
		||||
    color: var(--theme-color);
 | 
			
		||||
    background-color: var(--bs-body-bg);
 | 
			
		||||
    color: var(--bs-body-color);
 | 
			
		||||
  }
 | 
			
		||||
  .popover-body {
 | 
			
		||||
    color: var(--theme-color);
 | 
			
		||||
    color: var(--bs-body-color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
# syntax=docker/dockerfile:1
 | 
			
		||||
 | 
			
		||||
FROM node:16.16.0-bullseye
 | 
			
		||||
FROM node:18.17.0-bullseye
 | 
			
		||||
 | 
			
		||||
ENV NODE_ENV=development
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ function earn ({ models }) {
 | 
			
		||||
    console.log('running', name)
 | 
			
		||||
 | 
			
		||||
    // compute how much sn earned today
 | 
			
		||||
    let [{ sum }] = await models.$queryRaw`
 | 
			
		||||
    const [{ sum: actSum }] = await models.$queryRaw`
 | 
			
		||||
        SELECT coalesce(sum("ItemAct".msats - coalesce("ReferralAct".msats, 0)), 0) as sum
 | 
			
		||||
        FROM "ItemAct"
 | 
			
		||||
        JOIN "Item" ON "ItemAct"."itemId" = "Item".id
 | 
			
		||||
@ -21,7 +21,24 @@ function earn ({ models }) {
 | 
			
		||||
      SELECT coalesce(sum(sats), 0) as sum
 | 
			
		||||
      FROM "Donation"
 | 
			
		||||
      WHERE created_at > now_utc() - INTERVAL '1 day'`
 | 
			
		||||
    sum += donatedSum * 1000n
 | 
			
		||||
 | 
			
		||||
    // XXX prisma returns wonky types from raw queries ... so be extra
 | 
			
		||||
    // careful with them
 | 
			
		||||
    const sum = Number(actSum) + (Number(donatedSum) * 1000)
 | 
			
		||||
 | 
			
		||||
    if (sum <= 0) {
 | 
			
		||||
      console.log('done', name, 'no sats to award today')
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // extra sanity check on rewards ... if it more than 1m sats, we
 | 
			
		||||
    // probably have a bug somewhere
 | 
			
		||||
    if (sum > 1000000000) {
 | 
			
		||||
      console.log('done', name, 'error: too many sats to award today')
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(name, 'giving away', sum, 'msats')
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
      How earnings (used to) work:
 | 
			
		||||
@ -36,11 +53,6 @@ function earn ({ models }) {
 | 
			
		||||
      Now: 100% of earnings go to zappers of the top 21% of posts/comments
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    if (sum <= 0) {
 | 
			
		||||
      console.log('done', name, 'no earning')
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // get earners { userId, id, type, rank, proportion }
 | 
			
		||||
    const earners = await models.$queryRawUnsafe(`
 | 
			
		||||
      WITH item_ratios AS (
 | 
			
		||||
@ -88,19 +100,21 @@ function earn ({ models }) {
 | 
			
		||||
    const now = new Date(new Date().getTime())
 | 
			
		||||
 | 
			
		||||
    // this is just a sanity check because it seems like a good idea
 | 
			
		||||
    let total = 0n
 | 
			
		||||
    let total = 0
 | 
			
		||||
 | 
			
		||||
    // for each earner, serialize earnings
 | 
			
		||||
    // we do this for each earner because we don't need to serialize
 | 
			
		||||
    // all earner updates together
 | 
			
		||||
    earners.forEach(async earner => {
 | 
			
		||||
      const earnings = BigInt(Math.floor(earner.proportion * sum))
 | 
			
		||||
      const earnings = Math.floor(parseFloat(earner.proportion) * sum)
 | 
			
		||||
      total += earnings
 | 
			
		||||
      if (total > sum) {
 | 
			
		||||
        console.log('total exceeds sum', name)
 | 
			
		||||
        console.log(name, 'total exceeds sum', total, '>', sum)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log('stacker', earner.userId, 'earned', earnings)
 | 
			
		||||
 | 
			
		||||
      if (earnings > 0) {
 | 
			
		||||
        await serialize(models,
 | 
			
		||||
          models.$executeRaw`SELECT earn(${earner.userId}::INTEGER, ${earnings},
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ function checkWithdrawal ({ boss, models, lnd }) {
 | 
			
		||||
      const fee = Number(wdrwl.payment.fee_mtokens)
 | 
			
		||||
      const paid = Number(wdrwl.payment.mtokens) - fee
 | 
			
		||||
      await serialize(models, models.$executeRaw`
 | 
			
		||||
      SELECT confirm_withdrawl(${id}, ${paid}, ${fee})`)
 | 
			
		||||
      SELECT confirm_withdrawl(${id}::INTEGER, ${paid}, ${fee})`)
 | 
			
		||||
    } else if (wdrwl?.is_failed || notFound) {
 | 
			
		||||
      let status = 'UNKNOWN_FAILURE'
 | 
			
		||||
      if (wdrwl?.failed.is_insufficient_balance) {
 | 
			
		||||
@ -73,7 +73,7 @@ function checkWithdrawal ({ boss, models, lnd }) {
 | 
			
		||||
        status = 'ROUTE_NOT_FOUND'
 | 
			
		||||
      }
 | 
			
		||||
      await serialize(models, models.$executeRaw`
 | 
			
		||||
      SELECT reverse_withdrawl(${id}, ${status})`)
 | 
			
		||||
      SELECT reverse_withdrawl(${id}::INTEGER, ${status}::"WithdrawlStatus")`)
 | 
			
		||||
    } else {
 | 
			
		||||
      // we need to requeue to check again in 5 seconds
 | 
			
		||||
      await boss.send('checkWithdrawal', { id, hash }, walletOptions)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user