Merge branch 'master' into 266-zaps-without-account
This commit is contained in:
commit
3d0bb4b32c
|
@ -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,21 +31,25 @@ 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 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('/')
|
||||
}
|
||||
return null
|
||||
}}
|
||||
>
|
||||
<BackArrow className='theme me-1 me-md-2' width={22} height={22} />
|
||||
</a>
|
||||
}
|
||||
|
||||
function NotificationBell () {
|
||||
const { data } = useQuery(HAS_NOTIFICATIONS, {
|
||||
const { data } = useQuery(HAS_NOTIFICATIONS, SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: 30000,
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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({
|
||||
onClick={() => {
|
||||
const { nodata, ...query } = router.query
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { callbackUrl: router.query.callbackUrl, type: provider.name.toLowerCase() }
|
||||
})}
|
||||
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} />
|
||||
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} />)
|
||||
}
|
||||
console.error('__typename not supported:', n.__typename)
|
||||
return null
|
||||
</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>
|
||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||
</div>
|
||||
</NotificationLayout>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
someone joined via one of your referral links
|
||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||
</small>
|
||||
</NotificationLayout>
|
||||
)
|
||||
}
|
||||
|
||||
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,13 +257,12 @@ 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} />
|
||||
|
@ -253,11 +274,10 @@ function Reply ({ n }) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</NotificationLayout>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
})
|
||||
}, [client])
|
||||
}, 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,9 +20,13 @@ export function PriceProvider ({ price, children }) {
|
|||
const fiatCurrency = me?.fiatCurrency
|
||||
const { data } = useQuery(PRICE, {
|
||||
variables: { fiatCurrency },
|
||||
...(SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: 30000,
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
})
|
||||
})
|
||||
|
||||
const contextValue = {
|
||||
price: data?.price || price,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
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'
|
||||
|
|
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,10 +4,13 @@ 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, {
|
||||
const { data, error } = useQuery(INVOICE, SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: 1000,
|
||||
variables: { id: router.query.id },
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
|
|
|
@ -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} />
|
||||
</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={() => {
|
||||
{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>}
|
||||
}} 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,7 +32,9 @@ export function WithdrawlSkeleton ({ status }) {
|
|||
|
||||
function LoadWithdrawl () {
|
||||
const router = useRouter()
|
||||
const { loading, error, data } = useQuery(WITHDRAWL, {
|
||||
const { loading, error, data } = useQuery(WITHDRAWL, SSR
|
||||
? {}
|
||||
: {
|
||||
variables: { id: router.query.id },
|
||||
pollInterval: 1000,
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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…
Reference in New Issue