Compare commits
No commits in common. "b4298ca8667d5272525b82c816fad2c4e27c1636" and "71ce403b0ce9ead5d0f0bdc3f2eeca9a95bbf475" have entirely different histories.
b4298ca866
...
71ce403b0c
@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "sndev",
|
||||
"hostRequirements": {
|
||||
"cpus": 4,
|
||||
"memory": "16gb",
|
||||
"storage": "32gb"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-azuretools.vscode-containers"
|
||||
]
|
||||
}
|
||||
},
|
||||
"containerEnv": {
|
||||
"CPU_SHARES_IMPORTANT": "1024",
|
||||
"CPU_SHARES_MODERATE": "512",
|
||||
"CPU_SHARES_LOW": "128"
|
||||
},
|
||||
"postAttachCommand": "./scripts/setup-codespaces.sh && source .env.local && ./sndev start"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "sndev MINIMAL",
|
||||
"hostRequirements": {
|
||||
"cpus": 4,
|
||||
"memory": "16gb",
|
||||
"storage": "32gb"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-azuretools.vscode-containers"
|
||||
]
|
||||
}
|
||||
},
|
||||
"containerEnv": {
|
||||
"CPU_SHARES_IMPORTANT": "1024",
|
||||
"CPU_SHARES_MODERATE": "512",
|
||||
"CPU_SHARES_LOW": "128",
|
||||
"COMPOSE_PROFILES": "minimal"
|
||||
},
|
||||
"postAttachCommand": "./scripts/setup-codespaces.sh && source .env.local && ./sndev start"
|
||||
}
|
||||
@ -107,10 +107,10 @@ DB_WORKER_CONNECTION_LIMIT=2
|
||||
DB_TRANSACTION_TIMEOUT=5000
|
||||
|
||||
# polling intervals
|
||||
NEXT_PUBLIC_FAST_POLL_INTERVAL_MS=1000
|
||||
NEXT_PUBLIC_NORMAL_POLL_INTERVAL_MS=30000
|
||||
NEXT_PUBLIC_LONG_POLL_INTERVAL_MS=60000
|
||||
NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL_MS=300000
|
||||
NEXT_PUBLIC_FAST_POLL_INTERVAL=1000
|
||||
NEXT_PUBLIC_NORMAL_POLL_INTERVAL=30000
|
||||
NEXT_PUBLIC_LONG_POLL_INTERVAL=60000
|
||||
NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL=300000
|
||||
|
||||
# containers can't use localhost, so we need to use the container name
|
||||
IMGPROXY_URL_DOCKER=http://imgproxy:8080
|
||||
@ -140,8 +140,8 @@ SN_LND_REST_PORT=8080
|
||||
SN_LND_GRPC_PORT=10009
|
||||
SN_LND_P2P_PORT=9735
|
||||
# docker exec -u lnd sn_lnd lncli newaddress p2wkh --unused
|
||||
SN_LND_PUBKEY=034fcbe80658a2b0e32d416ca34c91cf359f7010b3529582fce6b1deddfadb2ba6
|
||||
SN_LND_ADDR=bcrt1qq6m9009pl3nkhku5905hl8nktq8zggfa75tfcy
|
||||
SN_LND_PUBKEY=03dc0de8fbe29ef3d26554c615adfd17aaca959403c4e9ecebaac4b83978d86342
|
||||
SN_LND_ADDR=bcrt1qu6g49vrl8n4ay99hr04wefkfy2e8g0z4nc0sjw
|
||||
# sn_lndk stuff
|
||||
SN_LNDK_GRPC_PORT=10012
|
||||
|
||||
@ -149,8 +149,8 @@ SN_LNDK_GRPC_PORT=10012
|
||||
LND_REST_PORT=8081
|
||||
LND_GRPC_PORT=10010
|
||||
# docker exec -u lnd lnd lncli newaddress p2wkh --unused
|
||||
LND_ADDR=bcrt1qvqjlqw3zmpqh9cv2jr20n2qn7u4n6k7f74yw4p
|
||||
LND_PUBKEY=03eff113993bd7dbb43f5e923d5569ecefa43e3cc7ce5b5634a4b6854d4b287dfa
|
||||
LND_ADDR=bcrt1qfqau4ug9e6rtrvxrgclg58e0r93wshucumm9vu
|
||||
LND_PUBKEY=028093ae52e011d45b3e67f2e0f2cb6c3a1d7f88d2920d408f3ac6db3a56dc4b35
|
||||
|
||||
# cln container stuff
|
||||
CLN_REST_PORT=9092
|
||||
@ -167,8 +167,8 @@ ECLAIR_PUBKEY="02268c74cc07837041131474881f97d497706b89a29f939555da6d094b65bd5af
|
||||
ROUTER_LND_REST_PORT=8082
|
||||
ROUTER_LND_GRPC_PORT=10011
|
||||
# docker exec -u lnd router_lnd lncli newaddress p2wkh --unused
|
||||
ROUTER_LND_ADDR=bcrt1qx7envhw8q48huaspl2am9nack3ql0fnyvy7ygy
|
||||
ROUTER_LND_PUBKEY=02862c14ccfc566234fd2ccb61f6f134ba4fa06cd13272d47a18bf4579c2df3cf4
|
||||
ROUTER_LND_ADDR=bcrt1qfkmwfpwgn6wt0dd36s79x04swz8vleyafsdpdr
|
||||
ROUTER_LND_PUBKEY=02750991fbf62e57631888bc469fae69c5e658bd1d245d8ab95ed883517caa33c3
|
||||
|
||||
LNCLI_NETWORK=regtest
|
||||
|
||||
|
||||
@ -11,10 +11,10 @@ NEXT_PUBLIC_MEDIA_DOMAIN=m.stacker.news
|
||||
PUBLIC_URL=https://stacker.news
|
||||
SELF_URL=http://127.0.0.1:8080
|
||||
grpc_proxy=http://127.0.0.1:7050
|
||||
NEXT_PUBLIC_FAST_POLL_INTERVAL_MS=1000
|
||||
NEXT_PUBLIC_NORMAL_POLL_INTERVAL_MS=30000
|
||||
NEXT_PUBLIC_LONG_POLL_INTERVAL_MS=60000
|
||||
NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL_MS=300000
|
||||
NEXT_PUBLIC_FAST_POLL_INTERVAL=1000
|
||||
NEXT_PUBLIC_NORMAL_POLL_INTERVAL=30000
|
||||
NEXT_PUBLIC_LONG_POLL_INTERVAL=60000
|
||||
NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL=300000
|
||||
NEXT_PUBLIC_URL=https://stacker.news
|
||||
TOR_PROXY=http://127.0.0.1:7050/
|
||||
PRISMA_SLOW_LOGS_MS=50
|
||||
|
||||
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@ -21,6 +21,3 @@ _For example, a change is not backward compatible if you removed a GraphQL field
|
||||
|
||||
|
||||
**Did you introduce any new environment variables? If so, call them out explicitly here:**
|
||||
|
||||
|
||||
**Did you use AI for this? If so, how much did it assist you?**
|
||||
|
||||
@ -16,6 +16,4 @@ EXPOSE 3000
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --legacy-peer-deps --loglevel verbose
|
||||
|
||||
# run npm ci again because we're mounting node_modules in local dev
|
||||
CMD ["sh","-c","npm ci --legacy-peer-deps --loglevel verbose && npx prisma migrate dev && npm run dev"]
|
||||
CMD ["sh","-c","npm install --loglevel verbose --legacy-peer-deps && npx prisma migrate dev && npm run dev"]
|
||||
29
README.md
29
README.md
@ -36,34 +36,6 @@ Go to [localhost:3000](http://localhost:3000).
|
||||
|
||||
<br>
|
||||
|
||||
### GitHub Codespaces
|
||||
|
||||
[](https://codespaces.new/stackernews/stacker.news)
|
||||
|
||||
You can run Stacker News on Github Codespaces
|
||||
|
||||
#### Setup
|
||||
|
||||
1. Open the repository on GitHub and click the **"Code"** button
|
||||
2. Select the Codespaces tab and create a new codespace.
|
||||
- You can also configure your codespace to run select services based on `COMPOSE_PROFILES` as well as in a different region and machine type by clicking "..." and selecting "New with options...". Check [Modifying services](#modifying-services) for more information on `COMPOSE_PROFILES`
|
||||
3. Wait for the environment to set up (this may take several minutes the first time)
|
||||
4. Once ready, you'll see a terminal with the environment initialized
|
||||
|
||||
#### Usage
|
||||
|
||||
After the codespace is created, the development environment will be automatically set up and services started.
|
||||
|
||||
Access your running application at the URL shown in the forwarded ports panel (typically `https://your-codespace-name-3000.app.github.dev`).
|
||||
|
||||
#### Port Configuration
|
||||
|
||||
⚠️ **Important**: For various internal services and external access to work properly, you must set forwarded ports to **Public** in the Ports tab:
|
||||
|
||||
1. In your codespace, look for the "PORTS" tab in the bottom panel
|
||||
2. Click the lock icon to change visibility from "Private" to "Public"
|
||||
<br>
|
||||
|
||||
## Usage
|
||||
|
||||
Start the development environment
|
||||
@ -198,7 +170,6 @@ To add/remove DNS records you can now use `./sndev domains dns`. More on this [h
|
||||
# Table of Contents
|
||||
- [Getting started](#getting-started)
|
||||
- [Installation](#installation)
|
||||
- [GitHub Codespaces](#github-codespaces)
|
||||
- [Usage](#usage)
|
||||
- [Modifying services](#modifying-services)
|
||||
- [Running specific services](#running-specific-services)
|
||||
|
||||
@ -60,11 +60,17 @@ export async function estimateRouteFee ({ lnd, destination, tokens, mtokens, req
|
||||
timeout
|
||||
}, (err, res) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
if (res?.failure_reason) {
|
||||
reject(new Error(`Unable to estimate route: ${res.failure_reason}`))
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (res.failure_reason !== 'FAILURE_REASON_NONE' || res.routing_fee_msat < 0 || res.time_lock_delay <= 0) {
|
||||
return reject(new Error(`Unable to estimate route: ${res.failure_reason}`))
|
||||
if (res.routing_fee_msat < 0 || res.time_lock_delay <= 0) {
|
||||
reject(new Error('Unable to estimate route, excessive values: ' + JSON.stringify(res)))
|
||||
return
|
||||
}
|
||||
|
||||
resolve({
|
||||
|
||||
@ -49,12 +49,10 @@ export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio },
|
||||
FROM upload_fees(${me?.id || USER_ID.anon}::INTEGER, ${uploadIds}::INTEGER[]))
|
||||
+ ${satsToMsats(boost)}::INTEGER as cost`
|
||||
|
||||
// freebies currently only apply to bios (comments disabled). To keep togglable behavior,
|
||||
// respect user's disableFreebies preference for bios as well.
|
||||
// conditions: is a bio, cost <= baseCost, user logged in, insufficient balance & credits,
|
||||
// and user has not disabled freebies
|
||||
const freebie = bio && cost <= baseCost && !!me &&
|
||||
me?.msats < cost && me?.mcredits < cost && !me?.disableFreebies
|
||||
// sub allows freebies (or is a bio or a comment), cost is less than baseCost, not anon,
|
||||
// cost must be greater than user's balance, and user has not disabled freebies
|
||||
const freebie = (parentId || bio) && cost <= baseCost && !!me &&
|
||||
me?.msats < cost && !me?.disableFreebies && me?.mcredits < cost
|
||||
|
||||
return freebie ? BigInt(0) : BigInt(cost)
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ensureProtocol, removeTracking, stripTrailingSlash } from '@/lib/url'
|
||||
import { decodeCursor, nextCursorEncoded } from '@/lib/cursor'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
|
||||
import { ruleSet as publicationDateRuleSet } from '@/lib/timedate-scraper'
|
||||
import domino from 'domino'
|
||||
@ -54,8 +54,7 @@ function commentsOrderByClause (me, models, sort) {
|
||||
async function comments (me, models, item, sort, cursor) {
|
||||
const orderBy = commentsOrderByClause(me, models, sort)
|
||||
|
||||
// if we're logged in, there might be pending comments from us we want to show but weren't counted
|
||||
if (!me && item.nDirectComments === 0) {
|
||||
if (item.nDirectComments === 0) {
|
||||
return {
|
||||
comments: [],
|
||||
cursor: null
|
||||
@ -68,7 +67,7 @@ async function comments (me, models, item, sort, cursor) {
|
||||
// XXX what a mess
|
||||
let comments
|
||||
if (me) {
|
||||
const filter = ` AND ("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID' OR "Item"."userId" = ${me.id}) AND ("Item"."parentId" <> $1 OR "Item".created_at <= '${decodedCursor.time.toISOString()}'::TIMESTAMP(3)) `
|
||||
const filter = ` AND ("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID' OR "Item"."userId" = ${me.id}) AND "Item".created_at <= '${decodedCursor.time.toISOString()}'::TIMESTAMP(3) `
|
||||
if (item.ncomments > FULL_COMMENTS_THRESHOLD) {
|
||||
const [{ item_comments_zaprank_with_me_limited: limitedComments }] = await models.$queryRawUnsafe(
|
||||
'SELECT item_comments_zaprank_with_me_limited($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8, $9)',
|
||||
@ -81,7 +80,7 @@ async function comments (me, models, item, sort, cursor) {
|
||||
comments = fullComments
|
||||
}
|
||||
} else {
|
||||
const filter = ` AND ("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID') AND ("Item"."parentId" <> $1 OR "Item".created_at <= '${decodedCursor.time.toISOString()}'::TIMESTAMP(3)) `
|
||||
const filter = ` AND ("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID') AND "Item".created_at <= '${decodedCursor.time.toISOString()}'::TIMESTAMP(3) `
|
||||
if (item.ncomments > FULL_COMMENTS_THRESHOLD) {
|
||||
const [{ item_comments_limited: limitedComments }] = await models.$queryRawUnsafe(
|
||||
'SELECT item_comments_limited($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::INTEGER, $5::INTEGER, $6, $7)',
|
||||
@ -130,7 +129,6 @@ export async function getAd (parent, { sub, subArr = [], showNsfw = false }, { m
|
||||
'"Item"."parentId" IS NULL',
|
||||
'"Item".bio = false',
|
||||
'"Item".boost > 0',
|
||||
await filterClause(me, models),
|
||||
activeOrMine(),
|
||||
subClause(sub, 1, 'Item', me, showNsfw),
|
||||
muteClause(me))}
|
||||
@ -181,8 +179,7 @@ export async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ..
|
||||
COALESCE("ItemAct"."meDontLikeMsats", 0) as "meDontLikeMsats", b."itemId" IS NOT NULL AS "meBookmark",
|
||||
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward",
|
||||
to_jsonb("Sub".*) || jsonb_build_object('meMuteSub', "MuteSub"."userId" IS NOT NULL)
|
||||
|| jsonb_build_object('meSubscription', "SubSubscription"."userId" IS NOT NULL) as sub,
|
||||
"CommentsViewAt"."last_viewed_at" as "meCommentsViewedAt"
|
||||
|| jsonb_build_object('meSubscription', "SubSubscription"."userId" IS NOT NULL) as sub
|
||||
FROM (
|
||||
${query}
|
||||
) "Item"
|
||||
@ -194,7 +191,6 @@ export async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ..
|
||||
LEFT JOIN "Sub" ON "Sub"."name" = "Item"."subName"
|
||||
LEFT JOIN "MuteSub" ON "Sub"."name" = "MuteSub"."subName" AND "MuteSub"."userId" = ${me.id}
|
||||
LEFT JOIN "SubSubscription" ON "Sub"."name" = "SubSubscription"."subName" AND "SubSubscription"."userId" = ${me.id}
|
||||
LEFT JOIN "CommentsViewAt" ON "CommentsViewAt"."itemId" = "Item".id AND "CommentsViewAt"."userId" = ${me.id}
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT "itemId",
|
||||
sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM 'FAILED' AND "InvoiceForward".id IS NOT NULL AND (act = 'FEE' OR act = 'TIP')) AS "meMsats",
|
||||
@ -365,7 +361,7 @@ export default {
|
||||
|
||||
return count
|
||||
},
|
||||
items: async (parent, { sub, sort, type, cursor, name, when, from, to, by, limit }, { me, models }) => {
|
||||
items: async (parent, { sub, sort, type, cursor, name, when, from, to, by, limit = LIMIT }, { me, models }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
let items, user, pins, subFull, table, ad
|
||||
|
||||
@ -586,7 +582,7 @@ export default {
|
||||
break
|
||||
}
|
||||
return {
|
||||
cursor: items.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
|
||||
cursor: items.length === limit ? nextCursorEncoded(decodedCursor) : null,
|
||||
items,
|
||||
pins,
|
||||
ad
|
||||
@ -744,7 +740,7 @@ export default {
|
||||
subMaxBoost: subAgg?._max.boost || 0
|
||||
}
|
||||
},
|
||||
newComments: async (parent, { itemId, after }, { models, me }) => {
|
||||
newComments: async (parent, { rootId, after }, { models, me }) => {
|
||||
const comments = await itemQueryWithMeta({
|
||||
me,
|
||||
models,
|
||||
@ -758,7 +754,7 @@ export default {
|
||||
'"Item"."created_at" > $2'
|
||||
)}
|
||||
ORDER BY "Item"."created_at" ASC`
|
||||
}, Number(itemId), after)
|
||||
}, Number(rootId), after)
|
||||
|
||||
return { comments }
|
||||
}
|
||||
@ -1076,21 +1072,6 @@ export default {
|
||||
])
|
||||
|
||||
return result
|
||||
},
|
||||
updateCommentsViewAt: async (parent, { id, meCommentsViewedAt }, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new GqlAuthenticationError()
|
||||
}
|
||||
|
||||
const result = await models.commentsViewAt.upsert({
|
||||
where: {
|
||||
userId_itemId: { userId: Number(me.id), itemId: Number(id) }
|
||||
},
|
||||
update: { lastViewedAt: new Date(meCommentsViewedAt) },
|
||||
create: { userId: Number(me.id), itemId: Number(id), lastViewedAt: new Date(meCommentsViewedAt) }
|
||||
})
|
||||
|
||||
return result.lastViewedAt
|
||||
}
|
||||
},
|
||||
ItemAct: {
|
||||
@ -1241,8 +1222,7 @@ export default {
|
||||
return item.comments
|
||||
}
|
||||
|
||||
// if we're logged in, there might be pending comments from us we want to show but weren't counted
|
||||
if (!me && item.ncomments === 0) {
|
||||
if (item.ncomments === 0) {
|
||||
return {
|
||||
comments: [],
|
||||
cursor: null
|
||||
|
||||
@ -167,7 +167,6 @@ export default {
|
||||
) as "Item"
|
||||
${whereClause(
|
||||
'"Item".created_at < $2',
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
await filterClause(me, models),
|
||||
muteClause(me),
|
||||
activeOrMine(me))}
|
||||
|
||||
@ -2,7 +2,6 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||
import { whenToFrom } from '@/lib/time'
|
||||
import { getItem, itemQueryWithMeta, SELECT } from './item'
|
||||
import { parse } from 'tldts'
|
||||
import { searchSchema, validateSchema } from '@/lib/validate'
|
||||
|
||||
function queryParts (q) {
|
||||
const regex = /"([^"]*)"/gm
|
||||
@ -25,7 +24,7 @@ function queryParts (q) {
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
related: async (parent, { title, id, cursor, limit, minMatch }, { me, models, search }) => {
|
||||
related: async (parent, { title, id, cursor, limit = LIMIT, minMatch }, { me, models, search }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
|
||||
if (!id && (!title || title.trim().split(/\s+/).length < 1)) {
|
||||
@ -81,7 +80,7 @@ export default {
|
||||
{
|
||||
neural: {
|
||||
title_embedding: {
|
||||
query_text: qtitle,
|
||||
query_text: qtext,
|
||||
model_id: process.env.OPENSEARCH_MODEL_ID,
|
||||
k: decodedCursor.offset + LIMIT
|
||||
}
|
||||
@ -90,7 +89,7 @@ export default {
|
||||
{
|
||||
neural: {
|
||||
text_embedding: {
|
||||
query_text: qtext.slice(0, 100),
|
||||
query_text: qtitle,
|
||||
model_id: process.env.OPENSEARCH_MODEL_ID,
|
||||
k: decodedCursor.offset + LIMIT
|
||||
}
|
||||
@ -169,12 +168,11 @@ export default {
|
||||
})
|
||||
|
||||
return {
|
||||
cursor: items.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
|
||||
cursor: items.length === (limit || LIMIT) ? nextCursorEncoded(decodedCursor) : null,
|
||||
items
|
||||
}
|
||||
},
|
||||
search: async (parent, { q, cursor, sort, what, when, from: whenFrom, to: whenTo }, { me, models, search }) => {
|
||||
await validateSchema(searchSchema, { q })
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
let sitems = null
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import { notifyTerritoryTransfer } from '@/lib/webPush'
|
||||
import performPaidAction from '../paidAction'
|
||||
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
|
||||
import { uploadIdsFromText } from './upload'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
export async function getSub (parent, { name }, { models, me }) {
|
||||
if (!name) return null
|
||||
@ -37,15 +36,24 @@ export async function getSub (parent, { name }, { models, me }) {
|
||||
export default {
|
||||
Query: {
|
||||
sub: getSub,
|
||||
subSuggestions: async (parent, { q, limit }, { models }) => {
|
||||
subSuggestions: async (parent, { q, limit = 5 }, { models }) => {
|
||||
let subs = []
|
||||
subs = await models.$queryRaw`
|
||||
if (q) {
|
||||
subs = await models.$queryRaw`
|
||||
SELECT name
|
||||
FROM "Sub"
|
||||
WHERE status IN ('ACTIVE', 'GRACE')
|
||||
${q ? Prisma.sql`AND SIMILARITY(name, ${q}) > 0.1` : Prisma.empty}
|
||||
${q ? Prisma.sql`ORDER BY SIMILARITY(name, ${q}) DESC` : Prisma.sql`ORDER BY name ASC`}
|
||||
WHERE status = 'ACTIVE'
|
||||
AND SIMILARITY(name, ${q}) > 0.1
|
||||
ORDER BY SIMILARITY(name, ${q}) DESC
|
||||
LIMIT ${limit}`
|
||||
} else {
|
||||
subs = await models.$queryRaw`
|
||||
SELECT name
|
||||
FROM "Sub"
|
||||
WHERE status = 'ACTIVE'
|
||||
ORDER BY name ASC
|
||||
LIMIT ${limit}`
|
||||
}
|
||||
|
||||
return subs
|
||||
},
|
||||
@ -54,15 +62,14 @@ export default {
|
||||
const currentUser = await models.user.findUnique({ where: { id: me.id } })
|
||||
const showNsfw = currentUser ? currentUser.nsfwMode : false
|
||||
|
||||
return await models.$queryRaw`
|
||||
SELECT "Sub".*, "Sub".created_at as "createdAt", ss."userId" IS NOT NULL as "meSubscription", COALESCE(json_agg("MuteSub".*) FILTER (WHERE "MuteSub"."userId" IS NOT NULL), '[]') AS "MuteSub"
|
||||
return await models.$queryRawUnsafe(`
|
||||
SELECT "Sub".*, "Sub".created_at as "createdAt", COALESCE(json_agg("MuteSub".*) FILTER (WHERE "MuteSub"."userId" IS NOT NULL), '[]') AS "MuteSub"
|
||||
FROM "Sub"
|
||||
LEFT JOIN "SubSubscription" ss ON "Sub".name = ss."subName" AND ss."userId" = ${me.id}::INTEGER
|
||||
LEFT JOIN "MuteSub" ON "Sub".name = "MuteSub"."subName" AND "MuteSub"."userId" = ${me.id}::INTEGER
|
||||
WHERE status <> 'STOPPED' ${showNsfw ? Prisma.empty : Prisma.sql`AND "Sub"."nsfw" = FALSE`}
|
||||
GROUP BY "Sub".name, ss."userId", "MuteSub"."userId"
|
||||
WHERE status <> 'STOPPED' ${showNsfw ? '' : 'AND "Sub"."nsfw" = FALSE'}
|
||||
GROUP BY "Sub".name, "MuteSub"."userId"
|
||||
ORDER BY "Sub".name ASC
|
||||
`
|
||||
`)
|
||||
}
|
||||
|
||||
return await models.sub.findMany({
|
||||
@ -89,7 +96,7 @@ export default {
|
||||
|
||||
return latest?.createdAt
|
||||
},
|
||||
topSubs: async (parent, { cursor, when, by, from, to, limit }, { models, me }) => {
|
||||
topSubs: async (parent, { cursor, when, by, from, to, limit = LIMIT }, { models, me }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
const range = whenRange(when, from, to || decodeCursor.time)
|
||||
|
||||
@ -121,7 +128,7 @@ export default {
|
||||
subs
|
||||
}
|
||||
},
|
||||
userSubs: async (_parent, { name, cursor, when, by, from, to, limit }, { models, me }) => {
|
||||
userSubs: async (_parent, { name, cursor, when, by, from, to, limit = LIMIT }, { models, me }) => {
|
||||
if (!name) {
|
||||
throw new GqlInputError('must supply user name')
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ async function authMethods (user, args, { models, me }) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function topUsers (parent, { cursor, when, by, from, to, limit }, { models, me }) {
|
||||
export async function topUsers (parent, { cursor, when, by, from, to, limit = LIMIT }, { models, me }) {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
const range = whenRange(when, from, to || decodeCursor.time)
|
||||
|
||||
@ -213,7 +213,7 @@ export default {
|
||||
users
|
||||
}
|
||||
},
|
||||
userSuggestions: async (parent, { q, limit }, { models }) => {
|
||||
userSuggestions: async (parent, { q, limit = 5 }, { models }) => {
|
||||
let users = []
|
||||
if (q) {
|
||||
users = await models.$queryRaw`
|
||||
@ -287,7 +287,6 @@ export default {
|
||||
'r.created_at > $2',
|
||||
'r.created_at >= "ThreadSubscription".created_at',
|
||||
'r."userId" <> $1',
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
activeOrMine(me),
|
||||
await filterClause(me, models),
|
||||
muteClause(me),
|
||||
@ -605,7 +604,7 @@ export default {
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE (id > ${RESERVED_MAX_USER_ID} OR id IN (${USER_ID.anon}, ${USER_ID.delete}))
|
||||
AND SIMILARITY(name, ${q}) > ${Number(similarity) || 0.1} ORDER BY SIMILARITY(name, ${q}) DESC LIMIT ${Number(limit)}`
|
||||
AND SIMILARITY(name, ${q}) > ${Number(similarity) || 0.1} ORDER BY SIMILARITY(name, ${q}) DESC LIMIT ${Number(limit) || 5}`
|
||||
},
|
||||
userStatsActions: async (parent, { when, from, to }, { me, models }) => {
|
||||
const range = whenRange(when, from, to)
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
import { LIMIT } from '@/lib/cursor'
|
||||
|
||||
export default gql`
|
||||
extend type Query {
|
||||
items(sub: String, sort: String, type: String, cursor: String, name: String, when: String, from: String, to: String, by: String, limit: Limit! = ${LIMIT}): Items
|
||||
items(sub: String, sort: String, type: String, cursor: String, name: String, when: String, from: String, to: String, by: String, limit: Limit): Items
|
||||
item(id: ID!): Item
|
||||
pageTitleAndUnshorted(url: String!): TitleUnshorted
|
||||
dupes(url: String!): [Item!]
|
||||
related(cursor: String, title: String, id: ID, minMatch: String, limit: Limit! = ${LIMIT}): Items
|
||||
related(cursor: String, title: String, id: ID, minMatch: String, limit: Limit): Items
|
||||
search(q: String, sub: String, cursor: String, what: String, sort: String, when: String, from: String, to: String): Items
|
||||
auctionPosition(sub: String, id: ID, boost: Int): Int!
|
||||
boostPosition(sub: String, id: ID, boost: Int): BoostPositions!
|
||||
itemRepetition(parentId: ID): Int!
|
||||
newComments(itemId: ID, after: Date): Comments!
|
||||
newComments(rootId: ID, after: Date): Comments!
|
||||
}
|
||||
|
||||
type BoostPositions {
|
||||
@ -65,7 +64,6 @@ export default gql`
|
||||
act(id: ID!, sats: Int, act: String, hasSendWallet: Boolean): ItemActPaidAction!
|
||||
pollVote(id: ID!): PollVotePaidAction!
|
||||
toggleOutlaw(id: ID!): Item!
|
||||
updateCommentsViewAt(id: ID!, meCommentsViewedAt: Date!): Date
|
||||
}
|
||||
|
||||
type PollVoteResult {
|
||||
@ -151,6 +149,7 @@ export default gql`
|
||||
ncomments: Int!
|
||||
nDirectComments: Int!
|
||||
comments(sort: String, cursor: String): Comments!
|
||||
injected: Boolean!
|
||||
path: String
|
||||
position: Int
|
||||
prior: Int
|
||||
@ -173,7 +172,6 @@ export default gql`
|
||||
apiKey: Boolean
|
||||
invoice: Invoice
|
||||
cost: Int!
|
||||
meCommentsViewedAt: Date
|
||||
}
|
||||
|
||||
input ItemForwardInput {
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
import { LIMIT } from '@/lib/cursor'
|
||||
|
||||
export default gql`
|
||||
extend type Query {
|
||||
sub(name: String): Sub
|
||||
subLatestPost(name: String!): String
|
||||
subs: [Sub!]!
|
||||
topSubs(cursor: String, when: String, from: String, to: String, by: String, limit: Limit! = ${LIMIT}): Subs
|
||||
userSubs(name: String!, cursor: String, when: String, from: String, to: String, by: String, limit: Limit! = ${LIMIT}): Subs
|
||||
topSubs(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
||||
userSubs(name: String!, cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
||||
mySubscribedSubs(cursor: String): Subs
|
||||
subSuggestions(q: String!, limit: Limit! = 5): [Sub!]!
|
||||
subSuggestions(q: String!, limit: Limit): [Sub!]!
|
||||
}
|
||||
|
||||
type Subs {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
import { LIMIT } from '@/lib/cursor'
|
||||
|
||||
export default gql`
|
||||
extend type Query {
|
||||
@ -8,10 +7,10 @@ export default gql`
|
||||
user(id: ID, name: String): User
|
||||
users: [User!]
|
||||
nameAvailable(name: String!): Boolean!
|
||||
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit! = ${LIMIT}): UsersNullable!
|
||||
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): UsersNullable!
|
||||
topCowboys(cursor: String): UsersNullable!
|
||||
searchUsers(q: String!, limit: Limit! = 5, similarity: Float): [User!]!
|
||||
userSuggestions(q: String, limit: Limit! = 5): [User!]!
|
||||
searchUsers(q: String!, limit: Limit, similarity: Float): [User!]!
|
||||
userSuggestions(q: String, limit: Limit): [User!]!
|
||||
hasNewNotes: Boolean!
|
||||
mySubscribedUsers(cursor: String): Users!
|
||||
myMutedUsers(cursor: String): Users!
|
||||
|
||||
@ -55,12 +55,6 @@ const typeDefs = gql`
|
||||
apiKey: VaultEntryInput!
|
||||
): WalletSendBlink!
|
||||
|
||||
upsertWalletSendCLNRest(
|
||||
${shared},
|
||||
socket: String!,
|
||||
rune: VaultEntryInput!,
|
||||
): WalletSendCLNRest!
|
||||
|
||||
upsertWalletRecvBlink(
|
||||
${shared},
|
||||
currency: String!,
|
||||
@ -108,11 +102,6 @@ const typeDefs = gql`
|
||||
${shared}
|
||||
): WalletSendWebLN!
|
||||
|
||||
upsertWalletRecvClink(
|
||||
${shared},
|
||||
noffer: String!
|
||||
): WalletRecvClink!
|
||||
|
||||
# tests
|
||||
testWalletRecvNWC(
|
||||
url: String!
|
||||
@ -149,12 +138,9 @@ const typeDefs = gql`
|
||||
apiKey: String!
|
||||
): Boolean!
|
||||
|
||||
testWalletRecvClink(
|
||||
noffer: String!
|
||||
): Boolean!
|
||||
|
||||
# delete
|
||||
deleteWallet(id: ID!): Boolean
|
||||
removeWallet(id: ID!): Boolean
|
||||
removeWalletProtocol(id: ID!): Boolean
|
||||
|
||||
# crypto
|
||||
updateWalletEncryption(keyHash: String!, wallets: [WalletEncryptionUpdate!]!): Boolean
|
||||
@ -163,7 +149,7 @@ const typeDefs = gql`
|
||||
disablePassphraseExport: Boolean
|
||||
|
||||
# settings
|
||||
setWalletSettings(settings: WalletSettingsInput!): WalletSettings!
|
||||
setWalletSettings(settings: WalletSettingsInput!): Boolean
|
||||
setWalletPriorities(priorities: [WalletPriorityUpdate!]!): Boolean
|
||||
|
||||
# logs
|
||||
@ -227,7 +213,6 @@ const typeDefs = gql`
|
||||
| WalletSendBlink
|
||||
| WalletSendWebLN
|
||||
| WalletSendLNC
|
||||
| WalletSendCLNRest
|
||||
| WalletRecvNWC
|
||||
| WalletRecvLNbits
|
||||
| WalletRecvPhoenixd
|
||||
@ -235,7 +220,6 @@ const typeDefs = gql`
|
||||
| WalletRecvLightningAddress
|
||||
| WalletRecvCLNRest
|
||||
| WalletRecvLNDGRPC
|
||||
| WalletRecvClink
|
||||
|
||||
type WalletSettings {
|
||||
receiveCreditsBelowSats: Int!
|
||||
@ -290,12 +274,6 @@ const typeDefs = gql`
|
||||
serverHost: VaultEntry!
|
||||
}
|
||||
|
||||
type WalletSendCLNRest {
|
||||
id: ID!
|
||||
socket: String!
|
||||
rune: VaultEntry!
|
||||
}
|
||||
|
||||
type WalletRecvNWC {
|
||||
id: ID!
|
||||
url: String!
|
||||
@ -338,11 +316,6 @@ const typeDefs = gql`
|
||||
cert: String
|
||||
}
|
||||
|
||||
type WalletRecvClink {
|
||||
id: ID!
|
||||
noffer: String!
|
||||
}
|
||||
|
||||
input AutowithdrawSettings {
|
||||
autoWithdrawThreshold: Int!
|
||||
autoWithdrawMaxFeePercent: Float!
|
||||
|
||||
17
awards.csv
17
awards.csv
@ -256,18 +256,5 @@ brymut,pr,#2326,,good-first-issue,,,,20k,brymut@stacker.news,2025-07-31
|
||||
brymut,pr,#2332,#2276,easy,,,,100k,brymut@stacker.news,2025-07-31
|
||||
ed-kung,pr,#2373,#2371,good-first-issue,,,,20k,simplestacker@getalby.com,2025-07-31
|
||||
ed-kung,issue,#2373,#2371,good-first-issue,,,,2k,simplestacker@getalby.com,2025-07-31
|
||||
pory-gone,pr,#2381,#2370,good-first-issue,,,,20k,pory@porygone.xyz,2025-08-26
|
||||
pory-gone,pr,#2413,#2361,easy,,,,100k,pory@porygone.xyz,2025-08-26
|
||||
abhiShandy,pr,#2405,#2399,good-first-issue,,,,20k,abhishandy@stacker.news,2025-08-15
|
||||
ed-kung,pr,#2239,#2197,medium,,,,250k,simplestacker@getalby.com,2025-08-15
|
||||
pory-gone,pr,#2420,#2278,easy,,,,100k,pory@porygone.xyz,2025-08-26
|
||||
brymut,pr,#2248,#2209,easy,,,,100k,brymut@stacker.news,2025-08-26
|
||||
ed-kung,pr,#2213,#2208,medium,,,,250k,simplestacker@getalby.com,2025-08-26
|
||||
ed-kung,issue,#2213,#2208,medium,,,,25k,simplestacker@getalby.com,2025-08-26
|
||||
Scroogey-SN,pr,#2434,,good-first-issue,,,,20k,Scroogey@coinos.io,2025-08-26
|
||||
Scroogey-SN,pr,#2446,#2444,good-first-issue,,1,,18k,Scroogey@coinos.io,2025-09-03
|
||||
Scroogey-SN,pr,#2447,#2443,good-first-issue,,,,20k,Scroogey@coinos.io,2025-09-03
|
||||
Scroogey-SN,pr,#2451,#2392,good-first-issue,,,,20k,Scroogey@coinos.io,2025-09-03
|
||||
ed-kung,issue,#2432,#2427,medium,,,,25k,simplestacker@getalby.com,???
|
||||
pory-gone,pr,#2514,#2511,good-first-issue,,1,,18k,pory@porygone.xyz,???
|
||||
brymut,pr,#2566,#2555,good-first-issue,,,,20k,brymut@stacker.news,???
|
||||
pory-gone,pr,#2381,#2370,good-first-issue,,,,20k,pory@porygone.xyz,???
|
||||
pory-gone,pr,#2413,#2361,easy,,,,100k,pory@porygone.xyz,???
|
||||
|
||||
|
@ -32,7 +32,7 @@ const FormStatus = {
|
||||
|
||||
export function BoostHelp () {
|
||||
return (
|
||||
<ol>
|
||||
<ol style={{ lineHeight: 1.25 }}>
|
||||
<li>Boost ranks items higher based on the amount</li>
|
||||
<li>The highest boost in a territory over the last 30 days is pinned to the top of the territory</li>
|
||||
<li>The highest boost across all territories over the last 30 days is pinned to the top of the homepage</li>
|
||||
@ -45,7 +45,6 @@ export function BoostHelp () {
|
||||
</li>
|
||||
<li>boost can take a few minutes to show higher ranking in feed</li>
|
||||
<li>100% of boost goes to the territory founder and top stackers as rewards</li>
|
||||
<li>If a boost is outlawed, it will only be visible to stackers in wild west mode</li>
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,12 +5,11 @@ import CowboyHatIcon from '@/svgs/cowboy.svg'
|
||||
import AnonIcon from '@/svgs/spy-fill.svg'
|
||||
import GunIcon from '@/svgs/revolver.svg'
|
||||
import HorseIcon from '@/svgs/horse.svg'
|
||||
import BotIcon from '@/svgs/robot-2-fill.svg'
|
||||
import { numWithUnits } from '@/lib/format'
|
||||
import { USER_ID } from '@/lib/constants'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export default function Badges ({ user, badge, bot, className = 'ms-1', badgeClassName, spacingClassName = 'ms-1', height = 16, width = 16 }) {
|
||||
export default function Badges ({ user, badge, className = 'ms-1', badgeClassName, spacingClassName = 'ms-1', height = 16, width = 16 }) {
|
||||
if (!user || Number(user.id) === USER_ID.ad) return null
|
||||
if (Number(user.id) === USER_ID.anon) {
|
||||
return (
|
||||
@ -20,7 +19,7 @@ export default function Badges ({ user, badge, bot, className = 'ms-1', badgeCla
|
||||
)
|
||||
}
|
||||
|
||||
let badges = []
|
||||
const badges = []
|
||||
|
||||
const streak = user.optional.streak
|
||||
if (streak !== null) {
|
||||
@ -47,13 +46,6 @@ export default function Badges ({ user, badge, bot, className = 'ms-1', badgeCla
|
||||
})
|
||||
}
|
||||
|
||||
if (bot) {
|
||||
badges = [{
|
||||
icon: BotIcon,
|
||||
overlayText: 'posted as bot'
|
||||
}]
|
||||
}
|
||||
|
||||
if (badges.length === 0) return null
|
||||
|
||||
return (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createContext, useContext, useMemo } from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { NORMAL_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
import { BLOCK_HEIGHT } from '@/fragments/blockHeight'
|
||||
import { datePivot } from '@/lib/time'
|
||||
|
||||
@ -18,7 +18,7 @@ export const BlockHeightProvider = ({ blockHeight, children }) => {
|
||||
...(SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: NORMAL_POLL_INTERVAL_MS,
|
||||
pollInterval: NORMAL_POLL_INTERVAL,
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
})
|
||||
})
|
||||
|
||||
@ -56,9 +56,8 @@ function useArrowKeys ({ moveLeft, moveRight }) {
|
||||
function Carousel ({ close, mediaArr, src, setOptions }) {
|
||||
const [index, setIndex] = useState(mediaArr.findIndex(([key]) => key === src))
|
||||
const [currentSrc, canGoLeft, canGoRight] = useMemo(() => {
|
||||
if (index === -1) return [src, false, false]
|
||||
return [mediaArr[index][0], index > 0, index < mediaArr.length - 1]
|
||||
}, [src, mediaArr, index])
|
||||
}, [mediaArr, index])
|
||||
|
||||
useEffect(() => {
|
||||
if (index === -1) return
|
||||
@ -116,12 +115,8 @@ export function CarouselProvider ({ children }) {
|
||||
const showModal = useShowModal()
|
||||
|
||||
const showCarousel = useCallback(({ src }) => {
|
||||
// only show confirmed entries
|
||||
const confirmedEntries = Array.from(media.current.entries())
|
||||
.filter(([, entry]) => entry.confirmed)
|
||||
|
||||
showModal((close, setOptions) => {
|
||||
return <Carousel close={close} mediaArr={confirmedEntries} src={src} setOptions={setOptions} />
|
||||
return <Carousel close={close} mediaArr={Array.from(media.current.entries())} src={src} setOptions={setOptions} />
|
||||
}, {
|
||||
fullScreen: true,
|
||||
overflow: <CarouselOverflow {...media.current.get(src)} />
|
||||
@ -129,25 +124,14 @@ export function CarouselProvider ({ children }) {
|
||||
}, [showModal])
|
||||
|
||||
const addMedia = useCallback(({ src, originalSrc, rel }) => {
|
||||
media.current.set(src, { src, originalSrc, rel, confirmed: false })
|
||||
}, [])
|
||||
|
||||
const confirmMedia = useCallback((src) => {
|
||||
const mediaItem = media.current.get(src)
|
||||
if (mediaItem) {
|
||||
mediaItem.confirmed = true
|
||||
media.current.set(src, mediaItem)
|
||||
}
|
||||
media.current.set(src, { src, originalSrc, rel })
|
||||
}, [])
|
||||
|
||||
const removeMedia = useCallback((src) => {
|
||||
media.current.delete(src)
|
||||
}, [])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ showCarousel, addMedia, confirmMedia, removeMedia }),
|
||||
[showCarousel, addMedia, confirmMedia, removeMedia]
|
||||
)
|
||||
const value = useMemo(() => ({ showCarousel, addMedia, removeMedia }), [showCarousel, addMedia, removeMedia])
|
||||
return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createContext, useContext, useMemo } from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { NORMAL_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
import { CHAIN_FEE } from '@/fragments/chainFee'
|
||||
|
||||
export const ChainFeeContext = createContext({
|
||||
@ -14,7 +14,7 @@ export const ChainFeeProvider = ({ chainFee, children }) => {
|
||||
...(SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: NORMAL_POLL_INTERVAL_MS,
|
||||
pollInterval: NORMAL_POLL_INTERVAL,
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
})
|
||||
})
|
||||
|
||||
@ -96,9 +96,8 @@ export function CommentFlat ({ item, rank, siblingComments, ...props }) {
|
||||
}
|
||||
|
||||
export default function Comment ({
|
||||
item, children, replyOpen, includeParent, topLevel,
|
||||
rootText, noComments, noReply, truncate, depth, pin, setDisableRetry, disableRetry,
|
||||
navigator
|
||||
item, children, replyOpen, includeParent, topLevel, rootLastCommentAt,
|
||||
rootText, noComments, noReply, truncate, depth, pin, setDisableRetry, disableRetry
|
||||
}) {
|
||||
const [edit, setEdit] = useState()
|
||||
const { me } = useMe()
|
||||
@ -117,18 +116,13 @@ export default function Comment ({
|
||||
|
||||
const unsetOutline = () => {
|
||||
if (!ref.current) return
|
||||
const hasOutline = ref.current.classList.contains('outline-new-comment') || ref.current.classList.contains('outline-new-injected-comment')
|
||||
const hasOutlineUnset = ref.current.classList.contains('outline-new-comment-unset')
|
||||
|
||||
const classes = ref.current.classList
|
||||
const hasOutline = classes.contains('outline-new-comment')
|
||||
const hasLiveOutline = classes.contains('outline-new-live-comment')
|
||||
const hasOutlineUnset = classes.contains('outline-new-comment-unset')
|
||||
|
||||
// don't try to untrack and unset the outline if the comment is not outlined or we already unset the outline
|
||||
if (!(hasLiveOutline || hasOutline) || hasOutlineUnset) return
|
||||
|
||||
classes.add('outline-new-comment-unset')
|
||||
// untrack new comment and its descendants if it's not a live comment
|
||||
navigator?.untrackNewComment(ref, { includeDescendants: hasOutline })
|
||||
// don't try to unset the outline if the comment is not outlined or we already unset the outline
|
||||
if (hasOutline && !hasOutlineUnset) {
|
||||
ref.current.classList.add('outline-new-comment-unset')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -157,37 +151,29 @@ export default function Comment ({
|
||||
}, [item.id, cache, router.query.commentId])
|
||||
|
||||
useEffect(() => {
|
||||
// checking navigator because outlining should happen only on item pages
|
||||
if (!navigator || me?.id === item.user?.id) return
|
||||
if (me?.id === item.user?.id) return
|
||||
|
||||
const itemCreatedAt = new Date(item.createdAt).getTime()
|
||||
// it's a new comment if it was created after the last comment was viewed
|
||||
// or, in the case of live comments, after the last comment was created
|
||||
const isNewComment = (router.query.commentsViewedAt && itemCreatedAt > router.query.commentsViewedAt) ||
|
||||
(rootLastCommentAt && itemCreatedAt > new Date(rootLastCommentAt).getTime())
|
||||
if (!isNewComment) return
|
||||
|
||||
const meViewedAt = new Date(root.meCommentsViewedAt).getTime()
|
||||
const viewedAt = me?.id ? meViewedAt : router.query.commentsViewedAt
|
||||
|
||||
const isNewComment = viewedAt && itemCreatedAt > viewedAt
|
||||
// live comments are new regardless of me or anon view time
|
||||
const rootLast = new Date(root.lastCommentAt || root.createdAt).getTime()
|
||||
const isNewLiveComment = item.live && itemCreatedAt > (meViewedAt || rootLast)
|
||||
|
||||
if (!isNewComment && !isNewLiveComment) return
|
||||
|
||||
if (item.live) {
|
||||
// live comments (item.live) have to use a different class to outline every new comment
|
||||
ref.current.classList.add('outline-new-live-comment')
|
||||
if (item.injected) {
|
||||
// newly injected comments (item.injected) have to use a different class to outline every new comment
|
||||
ref.current.classList.add('outline-new-injected-comment')
|
||||
|
||||
// wait for the injection animation to end before removing its class
|
||||
ref.current.addEventListener('animationend', () => {
|
||||
ref.current.classList.remove(styles.liveComment)
|
||||
ref.current.classList.remove(styles.injectedComment)
|
||||
}, { once: true })
|
||||
// animate the live comment injection
|
||||
ref.current.classList.add(styles.liveComment)
|
||||
ref.current.classList.add(styles.injectedComment)
|
||||
} else {
|
||||
ref.current.classList.add('outline-new-comment')
|
||||
}
|
||||
|
||||
navigator.trackNewComment(ref, itemCreatedAt)
|
||||
}, [item.id, root.lastCommentAt, root.meCommentsViewedAt])
|
||||
}, [item.id, rootLastCommentAt])
|
||||
|
||||
const bottomedOut = depth === COMMENT_DEPTH_LIMIT || (item.comments?.comments.length === 0 && item.nDirectComments > 0)
|
||||
// Don't show OP badge when anon user comments on anon user posts
|
||||
@ -309,7 +295,7 @@ export default function Comment ({
|
||||
? (
|
||||
<>
|
||||
{item.comments.comments.map((item) => (
|
||||
<Comment depth={depth + 1} key={item.id} item={item} navigator={navigator} />
|
||||
<Comment depth={depth + 1} key={item.id} item={item} rootLastCommentAt={rootLastCommentAt} />
|
||||
))}
|
||||
{item.comments.comments.length < item.nDirectComments && (
|
||||
<div className={`d-block ${styles.comment} pb-2 ps-3`}>
|
||||
|
||||
@ -18,7 +18,9 @@
|
||||
|
||||
.dontLike {
|
||||
fill: #a5a5a5;
|
||||
margin-right: 2px;
|
||||
margin-right: .35rem;
|
||||
padding: 2px;
|
||||
margin-left: 1px;
|
||||
margin-top: 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -62,23 +64,19 @@
|
||||
|
||||
.children {
|
||||
margin-top: 0;
|
||||
margin-left: 15px;
|
||||
margin-left: 27px;
|
||||
}
|
||||
|
||||
.comments {
|
||||
margin-left: -15px;
|
||||
margin-left: -.75rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
.comments {
|
||||
margin-left: 0px;
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
.children {
|
||||
margin-left: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton .hunk {
|
||||
width: 100%;
|
||||
@ -139,30 +137,34 @@
|
||||
padding-top: .5rem;
|
||||
}
|
||||
|
||||
.liveComment {
|
||||
.newCommentDot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bs-primary);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
background-color: #80d3ff;
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
background-color: #007cbe;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
background-color: #80d3ff;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.injectedComment {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.commentNavigator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
gap: 0.2rem;
|
||||
padding-bottom: 0.2rem;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
/* prevent double tap from zooming */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.newCommentDot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #007cbe;
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import { useRouter } from 'next/router'
|
||||
import MoreFooter from './more-footer'
|
||||
import { FULL_COMMENTS_THRESHOLD } from '@/lib/constants'
|
||||
import useLiveComments from './use-live-comments'
|
||||
import { useCommentsNavigatorContext } from './use-comments-navigator'
|
||||
|
||||
export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, commentSats }) {
|
||||
const router = useRouter()
|
||||
@ -71,10 +70,7 @@ export default function Comments ({
|
||||
const router = useRouter()
|
||||
|
||||
// fetch new comments that arrived after the lastCommentAt, and update the item.comments field in cache
|
||||
useLiveComments(parentId, lastCommentAt || parentCreatedAt)
|
||||
|
||||
// new comments navigator, tracks new comments and provides navigation controls
|
||||
const { navigator } = useCommentsNavigatorContext()
|
||||
useLiveComments(parentId, lastCommentAt || parentCreatedAt, router.query.sort)
|
||||
|
||||
const pins = useMemo(() => comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position), [comments])
|
||||
|
||||
@ -98,11 +94,11 @@ export default function Comments ({
|
||||
: null}
|
||||
{pins.map(item => (
|
||||
<Fragment key={item.id}>
|
||||
<Comment depth={1} item={item} navigator={navigator} {...props} pin />
|
||||
<Comment depth={1} item={item} rootLastCommentAt={lastCommentAt || parentCreatedAt} {...props} pin />
|
||||
</Fragment>
|
||||
))}
|
||||
{comments.filter(({ position }) => !position).map(item => (
|
||||
<Comment depth={1} key={item.id} item={item} navigator={navigator} {...props} />
|
||||
<Comment depth={1} key={item.id} item={item} rootLastCommentAt={lastCommentAt || parentCreatedAt} {...props} />
|
||||
))}
|
||||
{ncomments > FULL_COMMENTS_THRESHOLD &&
|
||||
<MoreFooter
|
||||
|
||||
@ -7,7 +7,6 @@ import Flag from '@/svgs/flag-fill.svg'
|
||||
import { useMemo } from 'react'
|
||||
import getColor from '@/lib/rainbow'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import styles from './upvote.module.css'
|
||||
|
||||
export function DownZap ({ item, ...props }) {
|
||||
const { meDontLikeSats } = item
|
||||
@ -21,9 +20,7 @@ export function DownZap ({ item, ...props }) {
|
||||
<DownZapper
|
||||
item={item} As={({ ...oprops }) =>
|
||||
<div className='upvoteParent'>
|
||||
<div className={styles.upvoteWrapper}>
|
||||
<Flag {...props} {...oprops} style={style} />
|
||||
</div>
|
||||
<Flag {...props} {...oprops} style={style} />
|
||||
</div>}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
import { createContext, useContext, useMemo, useState } from 'react'
|
||||
import { useHasNewNotes } from './use-has-new-notes'
|
||||
import Head from 'next/head'
|
||||
|
||||
const FAVICONS = {
|
||||
default: '/favicon.png',
|
||||
notify: '/favicon-notify.png',
|
||||
comments: '/favicon-comments.png',
|
||||
notifyWithComments: '/favicon-notify-with-comments.png'
|
||||
}
|
||||
|
||||
const getFavicon = (hasNewNotes, hasNewComments) => {
|
||||
if (hasNewNotes && hasNewComments) return FAVICONS.notifyWithComments
|
||||
if (hasNewNotes) return FAVICONS.notify
|
||||
if (hasNewComments) return FAVICONS.comments
|
||||
return FAVICONS.default
|
||||
}
|
||||
|
||||
export const FaviconContext = createContext()
|
||||
|
||||
export default function FaviconProvider ({ children }) {
|
||||
const hasNewNotes = useHasNewNotes()
|
||||
const [hasNewComments, setHasNewComments] = useState(false)
|
||||
|
||||
const favicon = useMemo(() =>
|
||||
getFavicon(hasNewNotes, hasNewComments),
|
||||
[hasNewNotes, hasNewComments])
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
favicon,
|
||||
hasNewNotes,
|
||||
hasNewComments,
|
||||
setHasNewComments
|
||||
}), [favicon, hasNewNotes, hasNewComments, setHasNewComments])
|
||||
|
||||
return (
|
||||
<FaviconContext.Provider value={contextValue}>
|
||||
<Head>
|
||||
<link rel='shortcut icon' href={favicon} />
|
||||
</Head>
|
||||
{children}
|
||||
</FaviconContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useFavicon () {
|
||||
return useContext(FaviconContext)
|
||||
}
|
||||
@ -4,7 +4,7 @@ import ActionTooltip from './action-tooltip'
|
||||
import Info from './info'
|
||||
import styles from './fee-button.module.css'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { ANON_FEE_MULTIPLIER, FAST_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { ANON_FEE_MULTIPLIER, FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
import { numWithUnits } from '@/lib/format'
|
||||
import { useMe } from './me'
|
||||
import AnonIcon from '@/svgs/spy-fill.svg'
|
||||
@ -14,7 +14,7 @@ import { SubmitButton } from './form'
|
||||
|
||||
const FeeButtonContext = createContext()
|
||||
|
||||
export function postCommentBaseLineItems ({ baseCost = 1, comment = false, bio = false, me }) {
|
||||
export function postCommentBaseLineItems ({ baseCost = 1, comment = false, me }) {
|
||||
const anonCharge = me
|
||||
? {}
|
||||
: {
|
||||
@ -28,10 +28,10 @@ export function postCommentBaseLineItems ({ baseCost = 1, comment = false, bio =
|
||||
return {
|
||||
baseCost: {
|
||||
term: baseCost,
|
||||
label: `${bio ? 'bio' : (comment ? 'comment' : 'post')} cost`,
|
||||
label: `${comment ? 'comment' : 'post'} cost`,
|
||||
op: '_',
|
||||
modifier: (cost) => cost + baseCost,
|
||||
allowFreebies: bio
|
||||
allowFreebies: comment
|
||||
},
|
||||
...anonCharge
|
||||
}
|
||||
@ -45,7 +45,7 @@ export function postCommentUseRemoteLineItems ({ parentId } = {}) {
|
||||
return function useRemoteLineItems () {
|
||||
const [line, setLine] = useState({})
|
||||
|
||||
const { data } = useQuery(query, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL_MS, nextFetchPolicy: 'cache-and-network' })
|
||||
const { data } = useQuery(query, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
|
||||
|
||||
useEffect(() => {
|
||||
const repetition = data?.itemRepetition
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import Link from 'next/link'
|
||||
import { RewardLine } from '@/pages/rewards'
|
||||
import { LONG_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { LONG_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
|
||||
const REWARDS = gql`
|
||||
{
|
||||
@ -12,7 +12,7 @@ const REWARDS = gql`
|
||||
}`
|
||||
|
||||
export default function Rewards () {
|
||||
const { data } = useQuery(REWARDS, SSR ? { ssr: false } : { pollInterval: LONG_POLL_INTERVAL_MS, nextFetchPolicy: 'cache-and-network' })
|
||||
const { data } = useQuery(REWARDS, SSR ? { ssr: false } : { pollInterval: LONG_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
|
||||
const total = data?.rewards?.[0]?.total
|
||||
const time = data?.rewards?.[0]?.time
|
||||
return (
|
||||
|
||||
@ -12,13 +12,10 @@ import No from '@/svgs/no.svg'
|
||||
import Bolt from '@/svgs/bolt.svg'
|
||||
import Amboss from '@/svgs/amboss.svg'
|
||||
import Mempool from '@/svgs/bimi.svg'
|
||||
import Live from '@/svgs/chat-unread-fill.svg'
|
||||
import NoLive from '@/svgs/chat-off-fill.svg'
|
||||
import Rewards from './footer-rewards'
|
||||
import useDarkMode from './dark-mode'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import { useAnimationEnabled } from '@/components/animation'
|
||||
import { useLiveCommentsToggle } from './use-live-comments'
|
||||
|
||||
const RssPopover = (
|
||||
<Popover>
|
||||
@ -100,6 +97,13 @@ const SocialsPopover = (
|
||||
const ChatPopover = (
|
||||
<Popover>
|
||||
<Popover.Body style={{ fontWeight: 500, fontSize: '.9rem' }}>
|
||||
{/* <a
|
||||
href='https://tribes.sphinx.chat/t/stackerzchat' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
sphinx
|
||||
</a>
|
||||
<span className='mx-2 text-muted'> \ </span> */}
|
||||
<a
|
||||
href='https://t.me/k00bideh' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
@ -108,10 +112,10 @@ const ChatPopover = (
|
||||
</a>
|
||||
<span className='mx-2 text-muted'> \ </span>
|
||||
<a
|
||||
href='https://signal.group/#CjQKIEt57YiluJoTW3lZqaqAq6echCekEYFfg7eIua2X91nLEhA__6ALI9pkaY_McQqX0jm1' className='nav-link p-0 d-inline-flex'
|
||||
href='https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2F6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE%3D%40smp10.simplex.im%2FebLYaEFGjsD3uK4fpE326c5QI1RZSxau%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAV086Oj5yCsavWzIbRMCVuF6jq793Tt__rWvCec__viI%253D%26srv%3Drb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22cZwSGoQhyOUulzp7rwCdWQ%3D%3D%22%7D' className='nav-link p-0 d-inline-flex'
|
||||
target='_blank' rel='noreferrer'
|
||||
>
|
||||
signal
|
||||
simplex
|
||||
</a>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
@ -143,11 +147,8 @@ export default function Footer ({ links = true }) {
|
||||
|
||||
const [animationEnabled, toggleAnimation] = useAnimationEnabled()
|
||||
|
||||
const [disableLiveComments, toggleLiveComments] = useLiveCommentsToggle()
|
||||
|
||||
const DarkModeIcon = darkMode ? Sun : Moon
|
||||
const LnIcon = animationEnabled ? No : Bolt
|
||||
const LiveIcon = disableLiveComments ? Live : NoLive
|
||||
|
||||
const version = process.env.NEXT_PUBLIC_COMMIT_HASH
|
||||
|
||||
@ -163,9 +164,6 @@ export default function Footer ({ links = true }) {
|
||||
<ActionTooltip notForm overlayText={`${animationEnabled ? 'disable' : 'enable'} lightning animations`}>
|
||||
<LnIcon onClick={toggleAnimation} width={20} height={20} className='ms-2 fill-grey theme' suppressHydrationWarning />
|
||||
</ActionTooltip>
|
||||
<ActionTooltip notForm overlayText={`${disableLiveComments ? 'enable' : 'disable'} live comments`}>
|
||||
<LiveIcon onClick={toggleLiveComments} width={20} height={20} className='ms-2 fill-grey theme' suppressHydrationWarning />
|
||||
</ActionTooltip>
|
||||
</div>
|
||||
<div className='mb-0' style={{ fontWeight: 500 }}>
|
||||
<Rewards />
|
||||
@ -251,9 +249,6 @@ export default function Footer ({ links = true }) {
|
||||
<Link href='/ek' className='ms-1'>
|
||||
@ek
|
||||
</Link>
|
||||
<Link href='/sox' className='ms-1'>
|
||||
@sox
|
||||
</Link>
|
||||
<span className='ms-1'>&</span>
|
||||
<Link href='https://github.com/stackernews/stacker.news/graphs/contributors' className='ms-1' target='_blank' rel='noreferrer'>
|
||||
more
|
||||
|
||||
@ -1451,7 +1451,6 @@ export function MultiInput ({
|
||||
onChange, autoFocus, hideError, inputType = 'text',
|
||||
...props
|
||||
}) {
|
||||
const formik = useFormikContext()
|
||||
const [inputs, setInputs] = useState(new Array(length).fill(''))
|
||||
const inputRefs = useRef(new Array(length).fill(null))
|
||||
const [, meta, helpers] = useField({ name })
|
||||
@ -1550,7 +1549,7 @@ export function MultiInput ({
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
{hideError && formik.submitCount > 0 && meta.touched && meta.error && ( // custom error message is showed if hideError is true
|
||||
{hideError && meta.touched && meta.error && ( // custom error message is showed if hideError is true
|
||||
<BootstrapForm.Control.Feedback type='invalid' className='d-block'>
|
||||
{meta.error}
|
||||
</BootstrapForm.Control.Feedback>
|
||||
|
||||
@ -5,7 +5,7 @@ export default function CCInfo (props) {
|
||||
return (
|
||||
<Info {...props}>
|
||||
<h6>Why am I getting cowboy credits?</h6>
|
||||
<ul>
|
||||
<ul className='line-height-md'>
|
||||
<li>to receive sats, you must attach an <Link href='/wallets'>external receiving wallet</Link></li>
|
||||
<li>zappers may have chosen to send you CCs instead of sats</li>
|
||||
<li>if the zaps are split on a post, recipients will receive CCs regardless of their configured receiving wallet</li>
|
||||
|
||||
@ -5,7 +5,7 @@ export default function RewardSatsInfo (props) {
|
||||
return (
|
||||
<Info {...props}>
|
||||
<h6>Where did my sats come from?</h6>
|
||||
<ul>
|
||||
<ul className='line-height-md'>
|
||||
<li>you may have sats from before <Link href='/items/835465'>SN went not-custodial</Link></li>
|
||||
<li>sats also come from <Link href='/rewards'>daily rewards</Link> and territory revenue
|
||||
<ul>
|
||||
|
||||
@ -7,7 +7,7 @@ import PayerData from './payer-data'
|
||||
import Bolt11Info from './bolt11-info'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { INVOICE } from '@/fragments/invoice'
|
||||
import { FAST_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
import { WalletConfigurationError, WalletPaymentAggregateError } from '@/wallets/client/errors'
|
||||
import ItemJob from './item-job'
|
||||
import Item from './item'
|
||||
@ -24,7 +24,7 @@ export default function Invoice ({
|
||||
const { data, error } = useQuery(query, SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: FAST_POLL_INTERVAL_MS,
|
||||
pollInterval: FAST_POLL_INTERVAL,
|
||||
variables: { id },
|
||||
nextFetchPolicy: 'cache-and-network',
|
||||
skip: !poll
|
||||
|
||||
@ -11,6 +11,7 @@ import { useMe } from './me'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import { useEffect } from 'react'
|
||||
import Poll from './poll'
|
||||
import { commentsViewed } from '@/lib/new-comments'
|
||||
import Related from './related'
|
||||
import PastBounties from './past-bounties'
|
||||
import Check from '@/svgs/check-double-line.svg'
|
||||
@ -25,7 +26,6 @@ import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||
import classNames from 'classnames'
|
||||
import { CarouselProvider } from './carousel'
|
||||
import Embed from './embed'
|
||||
import useCommentsView from './use-comments-view'
|
||||
|
||||
function BioItem ({ item, handleClick }) {
|
||||
const { me } = useMe()
|
||||
@ -161,12 +161,9 @@ function ItemText ({ item }) {
|
||||
}
|
||||
|
||||
export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props }) {
|
||||
// no cache update here because we need to preserve the initial value
|
||||
const { markItemViewed } = useCommentsView(item.id, { updateCache: false })
|
||||
|
||||
useEffect(() => {
|
||||
markItemViewed(item)
|
||||
}, [item.id, markItemViewed])
|
||||
commentsViewed(item)
|
||||
}, [item.lastCommentAt])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -134,7 +134,7 @@ export default function ItemInfo ({
|
||||
{showUser &&
|
||||
<Link href={`/${item.user.name}`}>
|
||||
<UserPopover name={item.user.name}>@{item.user.name}</UserPopover>
|
||||
<Badges badgeClassName='fill-grey' spacingClassName='ms-xs' height={12} width={12} user={item.user} bot={item.apiKey} />
|
||||
<Badges badgeClassName='fill-grey' spacingClassName='ms-xs' height={12} width={12} user={item.user} />
|
||||
{embellishUser}
|
||||
</Link>}
|
||||
<span> </span>
|
||||
@ -166,6 +166,9 @@ export default function ItemInfo ({
|
||||
{' '}<Badge className={styles.newComment} bg={null}>freebie</Badge>
|
||||
</Link>
|
||||
)}
|
||||
{(item.apiKey &&
|
||||
<>{' '}<Badge className={styles.newComment} bg={null}>bot</Badge></>
|
||||
)}
|
||||
{extraBadges}
|
||||
{
|
||||
showActionDropdown &&
|
||||
|
||||
@ -76,9 +76,10 @@ a.title:visited {
|
||||
|
||||
.dontLike {
|
||||
fill: #a5a5a5;
|
||||
margin-right: .35rem;
|
||||
margin-left: -.2rem;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.case {
|
||||
|
||||
@ -114,7 +114,7 @@ export default function JobForm ({ item, sub }) {
|
||||
label={
|
||||
<div className='d-flex align-items-center'>boost
|
||||
<Info>
|
||||
<ol>
|
||||
<ol className='line-height-md'>
|
||||
<li>Boost ranks jobs higher based on the amount</li>
|
||||
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
|
||||
<li>Boost must be divisible by {numWithUnits(BOOST_MULT, { abbreviate: false })}</li>
|
||||
|
||||
@ -9,7 +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 { FAST_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
|
||||
function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
|
||||
const query = gql`
|
||||
@ -19,7 +19,7 @@ function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
|
||||
k1
|
||||
}
|
||||
}`
|
||||
const { data } = useQuery(query, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL_MS, nextFetchPolicy: 'cache-and-network' })
|
||||
const { data } = useQuery(query, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.lnAuth?.pubkey) {
|
||||
|
||||
@ -75,8 +75,6 @@ export default function Login ({ providers, callbackUrl, multiAuth, error, text,
|
||||
const [errorMessage, setErrorMessage] = useState(authErrorMessage(error, signin))
|
||||
const router = useRouter()
|
||||
|
||||
multiAuth = typeof multiAuth === 'string' ? multiAuth === 'true' : !!multiAuth
|
||||
|
||||
// signup/signin awareness cookie
|
||||
useEffect(() => {
|
||||
// expire cookie if we're on /signup instead of /login
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { ME } from '@/fragments/users'
|
||||
import { FAST_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
|
||||
export const MeContext = React.createContext({
|
||||
me: null
|
||||
})
|
||||
|
||||
export function MeProvider ({ me, children }) {
|
||||
const { data, refetch } = useQuery(ME, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL_MS, nextFetchPolicy: 'cache-and-network' })
|
||||
const { data, refetch } = useQuery(ME, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
|
||||
// this makes sure that we always use the fetched data if it's null.
|
||||
// without this, we would always fallback to the `me` object
|
||||
// which was passed during page load which (visually) breaks switching to anon
|
||||
|
||||
@ -75,19 +75,12 @@ const Media = memo(function Media ({
|
||||
export default function MediaOrLink ({ linkFallback = true, ...props }) {
|
||||
const media = useMediaHelper(props)
|
||||
const [error, setError] = useState(false)
|
||||
const { showCarousel, addMedia, confirmMedia, removeMedia } = useCarousel()
|
||||
const { showCarousel, addMedia, removeMedia } = useCarousel()
|
||||
|
||||
// register placeholder immediately on mount if we have a src
|
||||
useEffect(() => {
|
||||
if (!media.bestResSrc) return
|
||||
addMedia({ src: media.bestResSrc, originalSrc: media.originalSrc, rel: props.rel })
|
||||
}, [addMedia, media.bestResSrc, media.originalSrc, props.rel])
|
||||
|
||||
// confirm media for carousel based on image detection
|
||||
useEffect(() => {
|
||||
if (!media.image) return
|
||||
confirmMedia(media.bestResSrc)
|
||||
}, [confirmMedia, media.image, media.bestResSrc])
|
||||
addMedia({ src: media.bestResSrc, originalSrc: media.originalSrc, rel: props.rel })
|
||||
}, [media.image])
|
||||
|
||||
const handleClick = useCallback(() => showCarousel({ src: media.bestResSrc }),
|
||||
[showCarousel, media.bestResSrc])
|
||||
@ -144,7 +137,7 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
|
||||
img.decode().then(() => { // decoding beforehand to prevent wrong image cropping
|
||||
setIsImage(true)
|
||||
}).catch((e) => {
|
||||
console.warn('Cannot decode image:', src, e)
|
||||
console.error('Cannot decode image', e)
|
||||
})
|
||||
}
|
||||
video.src = src
|
||||
|
||||
@ -1,224 +0,0 @@
|
||||
import { createContext, Fragment, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const MultiStepFormContext = createContext()
|
||||
|
||||
export function MultiStepForm ({ children, initial, steps }) {
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const [formState, setFormState] = useState({})
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
// initial state might not be available on first render so we sync changes
|
||||
if (initial) setFormState(initial)
|
||||
}, [initial])
|
||||
|
||||
useEffect(() => {
|
||||
const idx = Math.max(0, steps.indexOf(router.query.step))
|
||||
setStepIndex(idx)
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: { type: router.query.type, step: steps[idx] }
|
||||
}, null, { shallow: true })
|
||||
}, [router.query.step, steps])
|
||||
|
||||
const next = useCallback(() => {
|
||||
const idx = Math.min(stepIndex + 1, steps.length - 1)
|
||||
router.push(
|
||||
{ pathname: router.pathname, query: { type: router.query.type, step: steps[idx] } },
|
||||
null,
|
||||
{ shallow: true }
|
||||
)
|
||||
}, [stepIndex, steps, router])
|
||||
|
||||
const prev = useCallback(() => router.back(), [router])
|
||||
|
||||
const updateFormState = useCallback((id, state) => {
|
||||
setFormState(formState => {
|
||||
return id ? { ...formState, [id]: state } : state
|
||||
})
|
||||
}, [])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ stepIndex, steps, next, prev, formState, updateFormState }),
|
||||
[stepIndex, steps, next, prev, formState, updateFormState])
|
||||
return (
|
||||
<MultiStepFormContext.Provider value={value}>
|
||||
<Progress />
|
||||
{children[stepIndex]}
|
||||
</MultiStepFormContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Progress () {
|
||||
const steps = useSteps()
|
||||
const maxSteps = useMaxSteps()
|
||||
const stepIndex = useStepIndex()
|
||||
|
||||
const style = (index) => {
|
||||
switch (index) {
|
||||
case 0: return maxSteps === 2 ? { marginLeft: '-13px', marginRight: '-15px' } : { marginLeft: '-5px', marginRight: '-13px' }
|
||||
case 1: return { marginLeft: '-13px', marginRight: '-15px' }
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='d-flex my-3 mx-auto'>
|
||||
{
|
||||
steps.map((label, i) => {
|
||||
const last = i === steps.length - 1
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<ProgressNumber number={i + 1} label={label} active={stepIndex >= i} />
|
||||
{!last && <ProgressLine style={style(i)} active={stepIndex >= i + 1} />}
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function ProgressNumber ({ number, label, active }) {
|
||||
return (
|
||||
<div className={classNames('z-1 text-center', { 'text-info': active })}>
|
||||
<NumberSVG number={number} active={active} />
|
||||
<div className={classNames('small pt-1', active ? 'text-info' : 'text-muted')}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NUMBER_SVG_WIDTH = 24
|
||||
const NUMBER_SVG_HEIGHT = 24
|
||||
|
||||
function NumberSVG ({ number, active }) {
|
||||
const width = NUMBER_SVG_WIDTH
|
||||
const height = NUMBER_SVG_HEIGHT
|
||||
|
||||
const Wrapper = ({ children }) => (
|
||||
<div style={{ position: 'relative', width: `${width}px`, height: `${height}px`, margin: '0 auto' }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const Circle = () => {
|
||||
const circleProps = {
|
||||
fill: active ? 'var(--bs-info)' : 'var(--bs-body-bg)',
|
||||
stroke: active ? 'var(--bs-info)' : 'var(--theme-grey)'
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'
|
||||
width={width} height={height}
|
||||
style={{ position: 'absolute', top: 0, left: 0 }}
|
||||
>
|
||||
<circle cx='12' cy='12' r='11' strokeWidth='1' {...circleProps} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const Number = () => {
|
||||
const svgProps = {
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
viewBox: '0 0 24 24',
|
||||
// we scale the number down and render it in the center of the circle
|
||||
width: 0.5 * width,
|
||||
height: 0.5 * height,
|
||||
style: { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }
|
||||
}
|
||||
const numberColor = active ? 'var(--bs-white)' : 'var(--theme-grey)'
|
||||
// svgs are from https://remixicon.com/icon/number-1 etc.
|
||||
switch (number) {
|
||||
case 1:
|
||||
return (
|
||||
<svg {...svgProps}>
|
||||
<path fill={numberColor} d='M14 1.5V22H12V3.704L7.5 4.91V2.839L12.5 1.5H14Z' />
|
||||
</svg>
|
||||
)
|
||||
case 2:
|
||||
return (
|
||||
<svg {...svgProps}>
|
||||
<path
|
||||
fill={numberColor}
|
||||
d='M16.0002 7.5C16.0002 5.29086 14.2094 3.5 12.0002 3.5C9.7911 3.5 8.00024 5.29086 8.00024 7.5H6.00024C6.00024 4.18629 8.68653 1.5 12.0002 1.5C15.314 1.5 18.0002 4.18629 18.0002 7.5C18.0002 8.93092 17.4993 10.2448 16.6633 11.276L9.344 19.9991L18.0002 20V22H6.00024L6 20.8731L15.0642 10.071C15.6485 9.37595 16.0002 8.47905 16.0002 7.5Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 3:
|
||||
return (
|
||||
<svg {...svgProps}>
|
||||
<path fill={numberColor} d='M18.0001 2V3.36217L12.8087 9.54981C16.0169 9.94792 18.5001 12.684 18.5001 16C18.5001 19.5899 15.5899 22.5 12.0001 22.5C8.95434 22.5 6.39789 20.4052 5.69287 17.5778L7.63351 17.0922C8.12156 19.0497 9.89144 20.5 12.0001 20.5C14.4853 20.5 16.5001 18.4853 16.5001 16C16.5001 13.5147 14.4853 11.5 12.0001 11.5C11.2795 11.5 10.5985 11.6694 9.99465 11.9705L9.76692 12.0923L9.07705 10.8852L14.8551 3.99917L6.50006 4V2H18.0001Z' />
|
||||
</svg>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Circle />
|
||||
<Number />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function ProgressLine ({ style, active }) {
|
||||
const svgStyle = { display: 'block', position: 'relative', top: `${NUMBER_SVG_HEIGHT / 2}px` }
|
||||
return (
|
||||
<div style={style}>
|
||||
<svg style={svgStyle} width='100%' height='1' viewBox='0 0 100 1' preserveAspectRatio='none'>
|
||||
<path
|
||||
d='M 0 1 L 100 1'
|
||||
stroke={active ? 'var(--bs-info)' : 'var(--theme-grey)'}
|
||||
strokeWidth='1'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function useSteps () {
|
||||
const { steps } = useContext(MultiStepFormContext)
|
||||
return steps
|
||||
}
|
||||
|
||||
export function useStepIndex () {
|
||||
const { stepIndex } = useContext(MultiStepFormContext)
|
||||
return stepIndex
|
||||
}
|
||||
|
||||
export function useMaxSteps () {
|
||||
const steps = useSteps()
|
||||
return steps.length
|
||||
}
|
||||
|
||||
export function useStep () {
|
||||
const stepIndex = useStepIndex()
|
||||
const steps = useSteps()
|
||||
return steps[stepIndex]
|
||||
}
|
||||
|
||||
export function useNext () {
|
||||
const { next } = useContext(MultiStepFormContext)
|
||||
return next
|
||||
}
|
||||
|
||||
export function usePrev () {
|
||||
const { prev } = useContext(MultiStepFormContext)
|
||||
return prev
|
||||
}
|
||||
|
||||
export function useFormState (id) {
|
||||
const { formState, updateFormState } = useContext(MultiStepFormContext)
|
||||
const setFormState = useCallback(state => updateFormState(id, state), [id, updateFormState])
|
||||
return useMemo(
|
||||
() => [
|
||||
id ? formState[id] : formState,
|
||||
setFormState
|
||||
], [formState, id, setFormState])
|
||||
}
|
||||
@ -18,10 +18,12 @@ import SearchIcon from '../../svgs/search-line.svg'
|
||||
import classNames from 'classnames'
|
||||
import SnIcon from '@/svgs/sn.svg'
|
||||
import { useHasNewNotes } from '../use-has-new-notes'
|
||||
// import { useWallets } from '@/wallets/client/hooks'
|
||||
import { useWalletIndicator } from '@/wallets/client/hooks'
|
||||
import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account'
|
||||
import { useShowModal } from '@/components/modal'
|
||||
import { numWithUnits } from '@/lib/format'
|
||||
import Head from 'next/head'
|
||||
|
||||
export function Brand ({ className }) {
|
||||
return (
|
||||
@ -119,6 +121,9 @@ export function NavNotifications ({ className }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel='shortcut icon' href={hasNewNotes ? '/favicon-notify.png' : '/favicon.png'} />
|
||||
</Head>
|
||||
<Link href='/notifications' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='notifications' className={classNames('position-relative', className)}>
|
||||
<NoteIcon height={28} width={20} className='theme' />
|
||||
@ -159,20 +164,23 @@ export function NavWalletSummary ({ className }) {
|
||||
)
|
||||
}
|
||||
|
||||
export const Indicator = ({ show, children }) => {
|
||||
return (
|
||||
<div className='w-fit-content position-relative'>
|
||||
{children}
|
||||
{show && (
|
||||
<span className='d-inline-block p-1'>
|
||||
<span
|
||||
className='position-absolute p-1 bg-secondary'
|
||||
style={{ top: '5px', right: '0px', height: '5px', width: '5px' }}
|
||||
>
|
||||
<span className='invisible'>{' '}</span>
|
||||
</span>
|
||||
export const Indicator = ({ superscript }) => {
|
||||
if (superscript) {
|
||||
return (
|
||||
<span className='d-inline-block p-1'>
|
||||
<span
|
||||
className='position-absolute p-1 bg-secondary'
|
||||
style={{ top: '5px', right: '0px', height: '5px', width: '5px' }}
|
||||
>
|
||||
<span className='invisible'>{' '}</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='p-1 d-inline-block bg-secondary ms-1'>
|
||||
<span className='invisible'>{' '}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -190,7 +198,8 @@ export function MeDropdown ({ me, dropNavKey }) {
|
||||
<Dropdown.Toggle className='nav-link nav-item fw-normal' id='profile' variant='custom'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<Nav.Link eventKey={me.name} as='span' className='p-0 position-relative'>
|
||||
<Indicator show={indicator}>@{me.name}</Indicator>
|
||||
{`@${me.name}`}
|
||||
{indicator && <Indicator superscript />}
|
||||
</Nav.Link>
|
||||
<Badges user={me} />
|
||||
</div>
|
||||
@ -198,7 +207,8 @@ export function MeDropdown ({ me, dropNavKey }) {
|
||||
<Dropdown.Menu>
|
||||
<Link href={'/' + me.name} passHref legacyBehavior>
|
||||
<Dropdown.Item active={me.name === dropNavKey}>
|
||||
<Indicator show={profileIndicator}>profile</Indicator>
|
||||
profile
|
||||
{profileIndicator && <Indicator />}
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
|
||||
@ -206,7 +216,8 @@ export function MeDropdown ({ me, dropNavKey }) {
|
||||
</Link>
|
||||
<Link href='/wallets' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='wallets'>
|
||||
<Indicator show={walletIndicator}>wallets</Indicator>
|
||||
wallets
|
||||
{walletIndicator && <Indicator />}
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href='/credits' passHref legacyBehavior>
|
||||
@ -282,6 +293,7 @@ export default function LoginButton () {
|
||||
|
||||
function LogoutObstacle ({ onClose }) {
|
||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||
// const { removeLocalWallets } = useWallets()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
|
||||
@ -2,11 +2,9 @@ import { Nav, Navbar } from 'react-bootstrap'
|
||||
import styles from '../../header.module.css'
|
||||
import { AnonCorner, Back, Brand, MeCorner, NavPrice, SearchItem } from '../common'
|
||||
import { useMe } from '../../me'
|
||||
import { useCommentsNavigatorContext, CommentsNavigator } from '@/components/use-comments-navigator'
|
||||
|
||||
export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
||||
const { me } = useMe()
|
||||
const { navigator, commentCount } = useCommentsNavigatorContext()
|
||||
return (
|
||||
<Navbar>
|
||||
<Nav
|
||||
@ -17,7 +15,6 @@ export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
||||
<Brand className='me-1' />
|
||||
<SearchItem prefix={prefix} className='me-0 ms-2 d-none d-md-flex' />
|
||||
<NavPrice className='ms-auto me-0 mx-md-auto d-none d-md-flex' />
|
||||
<CommentsNavigator navigator={navigator} commentCount={commentCount} />
|
||||
{me
|
||||
? <MeCorner dropNavKey={dropNavKey} me={me} className='d-none d-md-flex' />
|
||||
: <AnonCorner path={path} className='d-none d-md-flex' />}
|
||||
|
||||
@ -28,11 +28,11 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
||||
|
||||
const profileIndicator = me && !me.bioId
|
||||
const walletIndicator = useWalletIndicator()
|
||||
const indicator = profileIndicator || walletIndicator
|
||||
|
||||
return (
|
||||
<>
|
||||
<Indicator show={indicator}><MeImage onClick={handleShow} /></Indicator>
|
||||
<MeImage onClick={handleShow} />
|
||||
|
||||
<Offcanvas className={canvasStyles.offcanvas} show={show} onHide={handleClose} placement='end'>
|
||||
<Offcanvas.Header closeButton>
|
||||
<Offcanvas.Title><NavWalletSummary /></Offcanvas.Title>
|
||||
@ -53,7 +53,8 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
||||
<>
|
||||
<Link href={'/' + me.name} passHref legacyBehavior>
|
||||
<Dropdown.Item active={me.name === dropNavKey}>
|
||||
<Indicator show={profileIndicator}>profile</Indicator>
|
||||
profile
|
||||
{profileIndicator && <Indicator />}
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
|
||||
@ -61,7 +62,8 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
||||
</Link>
|
||||
<Link href='/wallets' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='wallets'>
|
||||
<Indicator show={walletIndicator}>wallets</Indicator>
|
||||
wallets
|
||||
{walletIndicator && <Indicator />}
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href='/credits' passHref legacyBehavior>
|
||||
@ -91,7 +93,7 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
||||
<Link href={`/${me?.name || 'anon'}`} className='d-flex flex-row p-2 mt-auto text-muted'>
|
||||
<MeImage />
|
||||
<div className='ms-2'>
|
||||
<Indicator show={indicator}>@{me?.name || 'anon'}</Indicator>
|
||||
@{me?.name || 'anon'}
|
||||
</div>
|
||||
</Link>
|
||||
</Nav>
|
||||
|
||||
@ -2,12 +2,9 @@ import { Nav, Navbar } from 'react-bootstrap'
|
||||
import styles from '../../header.module.css'
|
||||
import { Back, NavPrice, NavSelect, NavWalletSummary, SignUpButton, hasNavSelect } from '../common'
|
||||
import { useMe } from '@/components/me'
|
||||
import { useCommentsNavigatorContext, CommentsNavigator } from '@/components/use-comments-navigator'
|
||||
|
||||
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
|
||||
const { me } = useMe()
|
||||
const { navigator, commentCount } = useCommentsNavigatorContext()
|
||||
|
||||
return (
|
||||
<Navbar>
|
||||
<Nav
|
||||
@ -20,7 +17,6 @@ export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNa
|
||||
: (
|
||||
<>
|
||||
<NavPrice className='flex-shrink-1' />
|
||||
<CommentsNavigator navigator={navigator} commentCount={commentCount} className='px-2' />
|
||||
{me ? <NavWalletSummary /> : <SignUpButton width='fit-content' />}
|
||||
</>)}
|
||||
</Nav>
|
||||
|
||||
@ -4,12 +4,10 @@ import { Container, Nav, Navbar } from 'react-bootstrap'
|
||||
import { NavPrice, MeCorner, AnonCorner, SearchItem, Back, NavWalletSummary, Brand, SignUpButton } from './common'
|
||||
import { useMe } from '@/components/me'
|
||||
import classNames from 'classnames'
|
||||
import { CommentsNavigator, useCommentsNavigatorContext } from '../use-comments-navigator'
|
||||
|
||||
export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
||||
const ref = useRef()
|
||||
const { me } = useMe()
|
||||
const { navigator, commentCount } = useCommentsNavigatorContext()
|
||||
|
||||
useEffect(() => {
|
||||
const stick = () => {
|
||||
@ -39,7 +37,6 @@ export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey })
|
||||
<Brand className='me-1' />
|
||||
<SearchItem className='me-0 ms-2' />
|
||||
<NavPrice />
|
||||
<CommentsNavigator navigator={navigator} commentCount={commentCount} className='d-flex' />
|
||||
{me ? <MeCorner dropNavKey={dropNavKey} me={me} className='d-flex' /> : <AnonCorner path={path} className='d-flex' />}
|
||||
</Nav>
|
||||
</Navbar>
|
||||
@ -47,12 +44,11 @@ export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey })
|
||||
<Container className='px-sm-0 d-block d-md-none'>
|
||||
<Navbar className='py-0'>
|
||||
<Nav
|
||||
className={classNames(styles.navbarNav)}
|
||||
className={classNames(styles.navbarNav, 'justify-content-between')}
|
||||
activeKey={topNavKey}
|
||||
>
|
||||
<Back />
|
||||
<NavPrice className='flex-shrink-1' />
|
||||
<CommentsNavigator navigator={navigator} commentCount={commentCount} className='d-flex' />
|
||||
<NavPrice className='flex-shrink-1 flex-grow-0' />
|
||||
{me ? <NavWalletSummary className='px-2' /> : <SignUpButton width='fit-content' />}
|
||||
</Nav>
|
||||
</Navbar>
|
||||
|
||||
@ -4,7 +4,8 @@ export default function preserveScroll (callback) {
|
||||
|
||||
// if the scroll position is at the top, we don't need to preserve it, just call the callback
|
||||
if (scrollTop <= 0) {
|
||||
return callback()
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
// get a reference element at the center of the viewport to track if content is added above it
|
||||
@ -48,5 +49,5 @@ export default function preserveScroll (callback) {
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
|
||||
return callback()
|
||||
callback()
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { fixedDecimal } from '@/lib/format'
|
||||
import { useMe } from './me'
|
||||
import { PRICE } from '@/fragments/price'
|
||||
import { CURRENCY_SYMBOLS } from '@/lib/currency'
|
||||
import { NORMAL_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
import { useBlockHeight } from './block-height'
|
||||
import { useChainFee } from './chain-fee'
|
||||
import { CompactLongCountdown } from './countdown'
|
||||
@ -27,7 +27,7 @@ export function PriceProvider ({ price, children }) {
|
||||
...(SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: NORMAL_POLL_INTERVAL_MS,
|
||||
pollInterval: NORMAL_POLL_INTERVAL,
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
})
|
||||
})
|
||||
|
||||
@ -17,13 +17,6 @@ export default function PullToRefresh ({ children, className }) {
|
||||
setIsPWA(androidPWA || iosPWA)
|
||||
}
|
||||
|
||||
const clearPullDistance = () => {
|
||||
setPullDistance(0)
|
||||
document.body.style.marginTop = '0px'
|
||||
touchStartY.current = 0
|
||||
touchEndY.current = 0
|
||||
}
|
||||
|
||||
useEffect(checkPWA, [])
|
||||
|
||||
const handleTouchStart = useCallback((e) => {
|
||||
@ -35,13 +28,6 @@ export default function PullToRefresh ({ children, className }) {
|
||||
const handleTouchMove = useCallback((e) => {
|
||||
if (touchStartY.current === 0) return
|
||||
if (!isPWA) return
|
||||
|
||||
// if we're not at the top of the page after the touch start, reset the pull distance
|
||||
if (window.scrollY > 0) {
|
||||
clearPullDistance()
|
||||
return
|
||||
}
|
||||
|
||||
touchEndY.current = e.touches[0].clientY
|
||||
const distance = touchEndY.current - touchStartY.current
|
||||
setPullDistance(distance)
|
||||
@ -53,7 +39,10 @@ export default function PullToRefresh ({ children, className }) {
|
||||
if (touchEndY.current - touchStartY.current > REFRESH_THRESHOLD) {
|
||||
router.push(router.asPath)
|
||||
}
|
||||
clearPullDistance()
|
||||
setPullDistance(0)
|
||||
document.body.style.marginTop = '0px'
|
||||
touchStartY.current = 0
|
||||
touchEndY.current = 0
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import { Form, MarkdownInput } from '@/components/form'
|
||||
import styles from './reply.module.css'
|
||||
import { COMMENTS } from '@/fragments/comments'
|
||||
import { useMe } from './me'
|
||||
import { forwardRef, useCallback, useEffect, useState, useRef, useMemo } from 'react'
|
||||
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
|
||||
import { commentsViewedAfterComment } from '@/lib/new-comments'
|
||||
import { commentSchema } from '@/lib/validate'
|
||||
import { ItemButtonBar } from './post'
|
||||
import { useShowModal } from './modal'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useRoot } from './root'
|
||||
import { CREATE_COMMENT } from '@/fragments/paidAction'
|
||||
import { injectComment } from '@/lib/comments'
|
||||
import useItemSubmit from './use-item-submit'
|
||||
import gql from 'graphql-tag'
|
||||
import useCommentsView from './use-comments-view'
|
||||
import { updateAncestorsCommentCount } from '@/lib/comments'
|
||||
|
||||
export default forwardRef(function Reply ({
|
||||
item,
|
||||
@ -29,7 +30,6 @@ export default forwardRef(function Reply ({
|
||||
const showModal = useShowModal()
|
||||
const root = useRoot()
|
||||
const sub = item?.sub || root?.sub
|
||||
const { markCommentViewedAt } = useCommentsView(root.id)
|
||||
|
||||
useEffect(() => {
|
||||
if (replyOpen || quote || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text')) {
|
||||
@ -51,11 +51,23 @@ export default forwardRef(function Reply ({
|
||||
update (cache, { data: { upsertComment: { result, invoice } } }) {
|
||||
if (!result) return
|
||||
|
||||
// inject the new comment into the cache
|
||||
const injected = injectComment(cache, result)
|
||||
if (injected) {
|
||||
markCommentViewedAt(result.createdAt, { ncomments: 1 })
|
||||
}
|
||||
cache.modify({
|
||||
id: `Item:${parentId}`,
|
||||
fields: {
|
||||
comments (existingComments = {}) {
|
||||
const newCommentRef = cache.writeFragment({
|
||||
data: result,
|
||||
fragment: COMMENTS,
|
||||
fragmentName: 'CommentsRecursive'
|
||||
})
|
||||
return {
|
||||
cursor: existingComments.cursor,
|
||||
comments: [newCommentRef, ...(existingComments?.comments || [])]
|
||||
}
|
||||
}
|
||||
},
|
||||
optimistic: true
|
||||
})
|
||||
|
||||
// no lag for itemRepetition
|
||||
if (!item.mine && me) {
|
||||
@ -67,6 +79,16 @@ export default forwardRef(function Reply ({
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const ancestors = item.path.split('.')
|
||||
|
||||
// update all ancestors
|
||||
updateAncestorsCommentCount(cache, ancestors, 1)
|
||||
|
||||
// so that we don't see indicator for our own comments, we record this comments as the latest time
|
||||
// but we also have record num comments, in case someone else commented when we did
|
||||
const root = ancestors[0]
|
||||
commentsViewedAfterComment(root, result.createdAt)
|
||||
}
|
||||
},
|
||||
onSuccessfulSubmit: (data, { resetForm }) => {
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
max-width: 600px;
|
||||
padding-right: 15px;
|
||||
padding-bottom: 1rem;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.replyButtons {
|
||||
@ -13,7 +12,6 @@
|
||||
align-items: center;
|
||||
line-height: 1rem;
|
||||
vertical-align: middle;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.replyButtons > * {
|
||||
@ -49,14 +47,4 @@
|
||||
.reply .text {
|
||||
margin-top: -1px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.replyButtons {
|
||||
margin-left: .99px;
|
||||
}
|
||||
|
||||
.reply {
|
||||
margin-left: .99px;
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,6 @@ import { useRouter } from 'next/router'
|
||||
import { whenToFrom } from '@/lib/time'
|
||||
import { useMe } from './me'
|
||||
import { useField } from 'formik'
|
||||
import { searchSchema } from '@/lib/validate'
|
||||
|
||||
export default function Search ({ sub }) {
|
||||
const router = useRouter()
|
||||
@ -66,7 +65,6 @@ export default function Search ({ sub }) {
|
||||
<Form
|
||||
initial={{ q, what, sort, when, from: '', to: '' }}
|
||||
onSubmit={values => search({ ...values })}
|
||||
schema={searchSchema}
|
||||
>
|
||||
<div className={`${styles.active} mb-3`}>
|
||||
<SearchInput
|
||||
|
||||
@ -22,6 +22,10 @@
|
||||
form>.active {
|
||||
display: flex;
|
||||
pointer-events: auto;
|
||||
flex-flow: row;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form>.active :global(.input-group) {
|
||||
flex-flow: nowrap;
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Select } from './form'
|
||||
import { EXTRA_LONG_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { EXTRA_LONG_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
import { SUBS } from '@/fragments/subs'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import styles from './sub-select.module.css'
|
||||
@ -24,7 +24,7 @@ export function useSubs ({ prependSubs = DEFAULT_PREPEND_SUBS, sub, filterSubs =
|
||||
const { data, refetch } = useQuery(SUBS, SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: EXTRA_LONG_POLL_INTERVAL_MS,
|
||||
pollInterval: EXTRA_LONG_POLL_INTERVAL,
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
})
|
||||
|
||||
|
||||
@ -2,8 +2,11 @@ import React, { useMemo, useState } from 'react'
|
||||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
import FormControl from 'react-bootstrap/FormControl'
|
||||
import TocIcon from '@/svgs/list-unordered.svg'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import { slug } from 'github-slugger'
|
||||
import { useRouter } from 'next/router'
|
||||
import { extractHeadings } from '@/lib/toc'
|
||||
|
||||
export default function Toc ({ text }) {
|
||||
const router = useRouter()
|
||||
@ -11,7 +14,16 @@ export default function Toc ({ text }) {
|
||||
return null
|
||||
}
|
||||
|
||||
const toc = useMemo(() => extractHeadings(text), [text])
|
||||
const toc = useMemo(() => {
|
||||
const tree = fromMarkdown(text)
|
||||
const toc = []
|
||||
visit(tree, 'heading', (node, position, parent) => {
|
||||
const str = toString(node)
|
||||
toc.push({ heading: str, slug: slug(str.replace(/[^\w\-\s]+/gi, '')), depth: node.depth })
|
||||
})
|
||||
|
||||
return toc
|
||||
}, [text])
|
||||
|
||||
if (toc.length === 0) {
|
||||
return null
|
||||
|
||||
@ -20,7 +20,6 @@ import rehypeSN from '@/lib/rehype-sn'
|
||||
import remarkUnicode from '@/lib/remark-unicode'
|
||||
import Embed from './embed'
|
||||
import remarkMath from 'remark-math'
|
||||
import remarkToc from '@/lib/remark-toc'
|
||||
|
||||
const rehypeSNStyled = () => rehypeSN({
|
||||
stylers: [{
|
||||
@ -34,11 +33,7 @@ const rehypeSNStyled = () => rehypeSN({
|
||||
}]
|
||||
})
|
||||
|
||||
const baseRemarkPlugins = [
|
||||
gfm,
|
||||
remarkUnicode,
|
||||
[remarkMath, { singleDollarTextMath: false }]
|
||||
]
|
||||
const remarkPlugins = [gfm, remarkUnicode, [remarkMath, { singleDollarTextMath: false }]]
|
||||
|
||||
export function SearchText ({ text }) {
|
||||
return (
|
||||
@ -54,9 +49,6 @@ export function SearchText ({ text }) {
|
||||
|
||||
// this is one of the slowest components to render
|
||||
export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, children, tab, itemId, outlawed, topLevel }) {
|
||||
// include remarkToc if topLevel
|
||||
const remarkPlugins = topLevel ? [...baseRemarkPlugins, remarkToc] : baseRemarkPlugins
|
||||
|
||||
// would the text overflow on the current screen size?
|
||||
const [overflowing, setOverflowing] = useState(false)
|
||||
// should we show the full text?
|
||||
@ -243,9 +235,9 @@ function MediaLink ({
|
||||
|
||||
function Table ({ node, ...props }) {
|
||||
return (
|
||||
<div className='table-responsive'>
|
||||
<span className='table-responsive'>
|
||||
<table className='table table-bordered table-sm' {...props} />
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -241,7 +241,6 @@
|
||||
|
||||
.text table {
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text blockquote {
|
||||
@ -449,4 +448,4 @@
|
||||
max-width: 480px;
|
||||
border-radius: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@ -20,8 +20,7 @@ export default function useCanEdit (item) {
|
||||
const anonEdit = !!invParams && !me && Number(item.user.id) === USER_ID.anon
|
||||
// anonEdit should not override canEdit, but only allow edits if they aren't already allowed
|
||||
setCanEdit(canEdit => canEdit || anonEdit)
|
||||
// update when the hmac gets set
|
||||
}, [item?.invoice?.hmac])
|
||||
}, [])
|
||||
|
||||
return [canEdit, setCanEdit, editThreshold]
|
||||
}
|
||||
|
||||
@ -1,200 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState, startTransition, createContext, useContext } from 'react'
|
||||
import styles from './comment.module.css'
|
||||
import LongPressable from './long-pressable'
|
||||
import { useFavicon } from './favicon'
|
||||
|
||||
const CommentsNavigatorContext = createContext({
|
||||
navigator: {
|
||||
trackNewComment: () => {},
|
||||
untrackNewComment: () => {},
|
||||
scrollToComment: () => {},
|
||||
clearCommentRefs: () => {}
|
||||
},
|
||||
commentCount: 0
|
||||
})
|
||||
|
||||
export function CommentsNavigatorProvider ({ children }) {
|
||||
const value = useCommentsNavigator()
|
||||
return (
|
||||
<CommentsNavigatorContext.Provider value={value}>
|
||||
{children}
|
||||
</CommentsNavigatorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useCommentsNavigatorContext () {
|
||||
return useContext(CommentsNavigatorContext)
|
||||
}
|
||||
|
||||
export function useCommentsNavigator () {
|
||||
const { setHasNewComments } = useFavicon()
|
||||
const [commentCount, setCommentCount] = useState(0)
|
||||
// refs in ref to not re-render on tracking
|
||||
const commentRefs = useRef([])
|
||||
// ref to track if the comment count is being updated
|
||||
const frameRef = useRef(null)
|
||||
const navigatorRef = useRef(null)
|
||||
|
||||
// batch updates to the comment count
|
||||
const throttleCountUpdate = useCallback(() => {
|
||||
if (frameRef.current) return
|
||||
// prevent multiple updates in the same frame
|
||||
frameRef.current = true
|
||||
window.requestAnimationFrame(() => {
|
||||
const next = commentRefs.current.length
|
||||
// transition to the new comment count
|
||||
startTransition?.(() => setCommentCount(next))
|
||||
frameRef.current = false
|
||||
})
|
||||
}, [])
|
||||
|
||||
// clear the list of refs and reset the comment count
|
||||
const clearCommentRefs = useCallback(() => {
|
||||
commentRefs.current = []
|
||||
startTransition?.(() => setCommentCount(0))
|
||||
setHasNewComments(false)
|
||||
}, [])
|
||||
|
||||
// track a new comment
|
||||
const trackNewComment = useCallback((commentRef, createdAt) => {
|
||||
setHasNewComments(true)
|
||||
try {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!commentRef?.current || !commentRef.current.isConnected) return
|
||||
|
||||
// dedupe
|
||||
const existing = commentRefs.current.some(item => item.ref.current === commentRef.current)
|
||||
if (existing) return
|
||||
|
||||
// find the correct insertion position to maintain sort order
|
||||
const insertIndex = commentRefs.current.findIndex(item => item.createdAt > createdAt)
|
||||
const newItem = { ref: commentRef, createdAt }
|
||||
|
||||
if (insertIndex === -1) {
|
||||
// append if no newer comments found
|
||||
commentRefs.current.push(newItem)
|
||||
} else {
|
||||
// insert at the correct position to maintain sort order
|
||||
commentRefs.current.splice(insertIndex, 0, newItem)
|
||||
}
|
||||
|
||||
throttleCountUpdate()
|
||||
})
|
||||
} catch {
|
||||
// in the rare case of a ref being disconnected during RAF, ignore to avoid blocking UI
|
||||
}
|
||||
}, [throttleCountUpdate])
|
||||
|
||||
// remove a comment ref from the list
|
||||
const untrackNewComment = useCallback((commentRef, options = {}) => {
|
||||
// we just need to read a single comment to clear the favicon
|
||||
setHasNewComments(false)
|
||||
|
||||
const { includeDescendants = false, clearOutline = false } = options
|
||||
|
||||
const refNode = commentRef.current
|
||||
if (!refNode) return
|
||||
|
||||
const toRemove = commentRefs.current.filter(item => {
|
||||
const node = item?.ref?.current
|
||||
return includeDescendants
|
||||
? node && refNode.contains(node)
|
||||
: node === refNode
|
||||
})
|
||||
|
||||
if (clearOutline) {
|
||||
for (const item of toRemove) {
|
||||
const node = item.ref.current
|
||||
if (!node) continue
|
||||
node.classList.remove(
|
||||
'outline-it',
|
||||
'outline-new-comment',
|
||||
'outline-new-live-comment'
|
||||
)
|
||||
node.classList.add('outline-new-comment-unset')
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.length) {
|
||||
commentRefs.current = commentRefs.current.filter(item => !toRemove.includes(item))
|
||||
throttleCountUpdate()
|
||||
}
|
||||
}, [throttleCountUpdate])
|
||||
|
||||
// scroll to the next new comment
|
||||
const scrollToComment = useCallback(() => {
|
||||
const list = commentRefs.current
|
||||
if (!list.length) return
|
||||
|
||||
const ref = list[0]?.ref
|
||||
const node = ref?.current
|
||||
if (!node) return
|
||||
|
||||
// smoothly scroll to the start of the comment
|
||||
node.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
|
||||
// clear the outline class after the animation ends
|
||||
node.addEventListener('animationend', () => {
|
||||
node.classList.remove('outline-it')
|
||||
}, { once: true })
|
||||
|
||||
// requestAnimationFrame to ensure untracking is processed before outlining
|
||||
window.requestAnimationFrame(() => {
|
||||
node.classList.add('outline-it')
|
||||
})
|
||||
|
||||
// untrack the new comment and clear the outlines
|
||||
untrackNewComment(ref, { includeDescendants: true, clearOutline: true })
|
||||
|
||||
// if we reached the end, reset the navigator
|
||||
if (list.length === 1) clearCommentRefs()
|
||||
}, [clearCommentRefs, untrackNewComment])
|
||||
|
||||
// create the navigator object once
|
||||
if (!navigatorRef.current) {
|
||||
navigatorRef.current = { trackNewComment, untrackNewComment, scrollToComment, clearCommentRefs }
|
||||
}
|
||||
|
||||
// clear the navigator on unmount
|
||||
useEffect(() => {
|
||||
return () => clearCommentRefs()
|
||||
}, [clearCommentRefs])
|
||||
|
||||
return { navigator: navigatorRef.current, commentCount }
|
||||
}
|
||||
|
||||
export function CommentsNavigator ({ navigator, commentCount, className }) {
|
||||
const { scrollToComment, clearCommentRefs } = navigator
|
||||
|
||||
const onNext = useCallback((e) => {
|
||||
// ignore if there are no new comments or if we're focused on a textarea or input
|
||||
if (!commentCount || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return
|
||||
// arrow right key scrolls to the next new comment
|
||||
if (e.key === 'ArrowRight' && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault()
|
||||
scrollToComment()
|
||||
}
|
||||
// escape key clears the new comments navigator
|
||||
if (e.key === 'Escape') clearCommentRefs()
|
||||
}, [commentCount, scrollToComment, clearCommentRefs])
|
||||
|
||||
useEffect(() => {
|
||||
if (!commentCount) return
|
||||
document.addEventListener('keydown', onNext)
|
||||
return () => document.removeEventListener('keydown', onNext)
|
||||
}, [onNext])
|
||||
|
||||
return (
|
||||
<LongPressable onShortPress={scrollToComment} onLongPress={clearCommentRefs}>
|
||||
<aside
|
||||
className={`${styles.commentNavigator} fw-bold nav-link ${className}`}
|
||||
style={{ visibility: commentCount ? 'visible' : 'hidden' }}
|
||||
>
|
||||
<span aria-label='next comment' className={styles.navigatorButton}>
|
||||
<div className={styles.newCommentDot} />
|
||||
</span>
|
||||
<span className=''>{commentCount}</span>
|
||||
</aside>
|
||||
</LongPressable>
|
||||
)
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
import { useMutation } from '@apollo/client'
|
||||
import { useCallback } from 'react'
|
||||
import { UPDATE_ITEM_USER_VIEW } from '@/fragments/items'
|
||||
import { commentsViewedAfterComment, commentsViewed, newComments } from '@/lib/new-comments'
|
||||
import { useMe } from './me'
|
||||
|
||||
export default function useCommentsView (itemId, { updateCache = true } = {}) {
|
||||
const { me } = useMe()
|
||||
|
||||
const [updateCommentsViewAt] = useMutation(UPDATE_ITEM_USER_VIEW, {
|
||||
update (cache, { data: { updateCommentsViewAt } }) {
|
||||
if (!updateCache || !itemId) return
|
||||
|
||||
cache.modify({
|
||||
id: `Item:${itemId}`,
|
||||
fields: { meCommentsViewedAt: () => updateCommentsViewAt }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const updateViewedAt = useCallback((latest, anonFallbackFn) => {
|
||||
if (me?.id) {
|
||||
updateCommentsViewAt({ variables: { id: Number(itemId), meCommentsViewedAt: latest } })
|
||||
} else {
|
||||
anonFallbackFn()
|
||||
}
|
||||
}, [me?.id, itemId, updateCommentsViewAt])
|
||||
|
||||
// update meCommentsViewedAt on comment injection
|
||||
const markCommentViewedAt = useCallback((latest, { ncomments } = {}) => {
|
||||
if (!latest) return
|
||||
|
||||
updateViewedAt(latest, () => commentsViewedAfterComment(itemId, latest, ncomments))
|
||||
}, [itemId, updateViewedAt])
|
||||
|
||||
// update meCommentsViewedAt on item view
|
||||
const markItemViewed = useCallback((item, latest) => {
|
||||
if (!item || item.parentId || (item?.meCommentsViewedAt && !newComments(item))) return
|
||||
const lastAt = latest || item?.lastCommentAt || item?.createdAt
|
||||
const newLatest = new Date(lastAt)
|
||||
|
||||
updateViewedAt(newLatest, () => commentsViewed(item))
|
||||
}, [updateViewedAt])
|
||||
|
||||
return { markCommentViewedAt, markItemViewed }
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { HAS_NOTIFICATIONS } from '@/fragments/notifications'
|
||||
import { NORMAL_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
|
||||
import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import React, { useContext } from 'react'
|
||||
import { clearNotifications } from '@/components/serviceworker'
|
||||
@ -11,7 +11,7 @@ export function HasNewNotesProvider ({ me, children }) {
|
||||
SSR
|
||||
? {}
|
||||
: {
|
||||
pollInterval: NORMAL_POLL_INTERVAL_MS,
|
||||
pollInterval: NORMAL_POLL_INTERVAL,
|
||||
nextFetchPolicy: 'cache-and-network',
|
||||
onCompleted: ({ hasNewNotes }) => {
|
||||
if (!hasNewNotes) {
|
||||
|
||||
@ -1,114 +1,124 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useQuery, useApolloClient } from '@apollo/client'
|
||||
import { SSR } from '../lib/constants'
|
||||
import preserveScroll from './preserve-scroll'
|
||||
import { GET_NEW_COMMENTS } from '../fragments/comments'
|
||||
import { injectComment } from '../lib/comments'
|
||||
import useCommentsView from './use-comments-view'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { SSR, COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
||||
import { useQuery, useApolloClient } from '@apollo/client'
|
||||
import { commentsViewedAfterComment } from '../lib/new-comments'
|
||||
import {
|
||||
updateItemQuery,
|
||||
updateCommentFragment,
|
||||
getLatestCommentCreatedAt,
|
||||
updateAncestorsCommentCount,
|
||||
calculateDepth
|
||||
} from '../lib/comments'
|
||||
|
||||
// live comments polling interval
|
||||
const POLL_INTERVAL = 1000 * 5
|
||||
// live comments toggle keys
|
||||
const STORAGE_DISABLE_KEY = 'disableLiveComments'
|
||||
const TOGGLE_EVENT = 'liveComments:toggle'
|
||||
const POLL_INTERVAL = 1000 * 5 // 5 seconds
|
||||
|
||||
const readStoredLatest = (key, latest) => {
|
||||
const stored = window.sessionStorage.getItem(key)
|
||||
return stored && stored > latest ? stored : latest
|
||||
// prepares and creates a fragment for injection into the cache
|
||||
// also handles side effects like updating comment counts and viewedAt timestamps
|
||||
function prepareComments (item, cache, newComment) {
|
||||
const existingComments = item.comments?.comments || []
|
||||
|
||||
// is the incoming new comment already in item's existing comments?
|
||||
// if so, we don't need to update the cache
|
||||
if (existingComments.some(comment => comment.id === newComment.id)) return item
|
||||
|
||||
// count the new comment (+1) and its children (+ncomments)
|
||||
const totalNComments = newComment.ncomments + 1
|
||||
|
||||
const itemHierarchy = item.path.split('.')
|
||||
// update all ancestors comment count, but not the item itself
|
||||
const ancestors = itemHierarchy.slice(0, -1)
|
||||
updateAncestorsCommentCount(cache, ancestors, totalNComments)
|
||||
// update commentsViewedAt to now, and add the number of new comments
|
||||
const rootId = itemHierarchy[0]
|
||||
commentsViewedAfterComment(rootId, Date.now(), totalNComments)
|
||||
|
||||
// add a flag to the new comment to indicate it was injected
|
||||
const injectedComment = { ...newComment, injected: true }
|
||||
|
||||
// an item can either have a comments.comments field, or not
|
||||
const payload = item.comments
|
||||
? {
|
||||
...item,
|
||||
ncomments: item.ncomments + totalNComments,
|
||||
comments: {
|
||||
...item.comments,
|
||||
comments: [injectedComment, ...item.comments.comments]
|
||||
}
|
||||
}
|
||||
// when the fragment doesn't have a comments field, we just update stats fields
|
||||
: {
|
||||
...item,
|
||||
ncomments: item.ncomments + totalNComments
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
// cache new comments and return the most recent timestamp between current latest and new comment
|
||||
// regardless of whether the comments were injected or not
|
||||
function cacheNewComments (cache, latest, itemId, newComments, markCommentViewedAt) {
|
||||
let injected = 0
|
||||
function cacheNewComments (cache, rootId, newComments, sort) {
|
||||
for (const newComment of newComments) {
|
||||
const { parentId } = newComment
|
||||
const topLevel = Number(parentId) === Number(rootId)
|
||||
|
||||
const injectedLatest = newComments.reduce((latestTimestamp, newComment) => {
|
||||
const result = injectComment(cache, newComment, { live: true, rootId: itemId })
|
||||
// if any comment was injected, increment injected
|
||||
injected = result ? injected + 1 : injected
|
||||
return new Date(newComment.createdAt) > new Date(latestTimestamp)
|
||||
? newComment.createdAt
|
||||
: latestTimestamp
|
||||
}, latest)
|
||||
|
||||
if (injected > 0) {
|
||||
markCommentViewedAt(injectedLatest, { ncomments: injected })
|
||||
// if the comment is a top level comment, update the item, else update the parent comment
|
||||
if (topLevel) {
|
||||
updateItemQuery(cache, rootId, sort, (item) => prepareComments(item, cache, newComment))
|
||||
} else {
|
||||
// if the comment is too deep, we can skip it
|
||||
const depth = calculateDepth(newComment.path, rootId, parentId)
|
||||
if (depth > COMMENT_DEPTH_LIMIT) continue
|
||||
// inject the new comment into the parent comment's comments field
|
||||
updateCommentFragment(cache, parentId, (parent) => prepareComments(parent, cache, newComment))
|
||||
}
|
||||
}
|
||||
|
||||
return injectedLatest
|
||||
}
|
||||
|
||||
// fetches comments for an item that are newer than the latest comment createdAt (after),
|
||||
// injects them into cache, and keeps scroll position stable.
|
||||
export default function useLiveComments (itemId, after) {
|
||||
const latestKey = `liveCommentsLatest:${itemId}`
|
||||
// useLiveComments fetches new comments under an item (rootId),
|
||||
// that are newer than the latest comment createdAt (after), and injects them into the cache.
|
||||
export default function useLiveComments (rootId, after, sort) {
|
||||
const latestKey = `liveCommentsLatest:${rootId}`
|
||||
const { cache } = useApolloClient()
|
||||
const { markCommentViewedAt } = useCommentsView(itemId)
|
||||
const [disableLiveComments] = useLiveCommentsToggle()
|
||||
|
||||
const [latest, setLatest] = useState(after)
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLatest(readStoredLatest(latestKey, after))
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedLatest = window.sessionStorage.getItem(latestKey)
|
||||
if (storedLatest && storedLatest > after) {
|
||||
setLatest(storedLatest)
|
||||
} else {
|
||||
setLatest(after)
|
||||
}
|
||||
}
|
||||
|
||||
// Apollo might update the cache before the page has fully rendered, causing reads of stale cached data
|
||||
// this prevents GET_NEW_COMMENTS from producing results before the page has fully rendered
|
||||
setInitialized(true)
|
||||
}, [itemId, after])
|
||||
}, [after])
|
||||
|
||||
const { data } = useQuery(GET_NEW_COMMENTS, {
|
||||
pollInterval: POLL_INTERVAL,
|
||||
// only get comments newer than the passed latest timestamp
|
||||
variables: { itemId, after: latest },
|
||||
nextFetchPolicy: 'cache-and-network',
|
||||
skip: SSR || !initialized || disableLiveComments
|
||||
})
|
||||
const { data } = useQuery(GET_NEW_COMMENTS, SSR || !initialized
|
||||
? {}
|
||||
: {
|
||||
pollInterval: POLL_INTERVAL,
|
||||
// only get comments newer than the passed latest timestamp
|
||||
variables: { rootId, after: latest },
|
||||
nextFetchPolicy: 'cache-and-network'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const newComments = data?.newComments?.comments
|
||||
if (!newComments?.length) return
|
||||
if (!data?.newComments?.comments?.length) return
|
||||
|
||||
// directly inject new comments into the cache, preserving scroll position
|
||||
// quirk: scroll is preserved even if we are not injecting new comments due to dedupe
|
||||
const injectedLatest = preserveScroll(() => cacheNewComments(cache, latest, itemId, newComments, markCommentViewedAt))
|
||||
|
||||
// if we didn't process any newer comments, bail
|
||||
if (new Date(injectedLatest).getTime() <= new Date(latest).getTime()) return
|
||||
preserveScroll(() => cacheNewComments(cache, rootId, data.newComments.comments, sort))
|
||||
|
||||
// update latest timestamp to the latest comment created at
|
||||
// save it to session storage, to persist between client-side navigations
|
||||
setLatest(injectedLatest)
|
||||
window.sessionStorage.setItem(latestKey, injectedLatest)
|
||||
}, [data, cache, itemId, latest, markCommentViewedAt])
|
||||
}
|
||||
|
||||
export function useLiveCommentsToggle () {
|
||||
const [disableLiveComments, setDisableLiveComments] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// preference: local storage
|
||||
const read = () => setDisableLiveComments(window.localStorage.getItem(STORAGE_DISABLE_KEY) === 'true')
|
||||
read()
|
||||
|
||||
// update across tabs
|
||||
const onStorage = e => { if (e.key === STORAGE_DISABLE_KEY) read() }
|
||||
// update this tab
|
||||
const onToggle = () => read()
|
||||
|
||||
window.addEventListener('storage', onStorage)
|
||||
window.addEventListener(TOGGLE_EVENT, onToggle)
|
||||
return () => {
|
||||
window.removeEventListener('storage', onStorage)
|
||||
window.removeEventListener(TOGGLE_EVENT, onToggle)
|
||||
const newLatest = getLatestCommentCreatedAt(data.newComments.comments, latest)
|
||||
setLatest(newLatest)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.sessionStorage.setItem(latestKey, newLatest)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
const current = window.localStorage.getItem(STORAGE_DISABLE_KEY) === 'true'
|
||||
window.localStorage.setItem(STORAGE_DISABLE_KEY, !current)
|
||||
// trigger local event to update this tab
|
||||
window.dispatchEvent(new Event(TOGGLE_EVENT))
|
||||
}, [])
|
||||
|
||||
return [disableLiveComments, toggle]
|
||||
}, [data, cache, rootId, sort, latest])
|
||||
}
|
||||
|
||||
@ -10,7 +10,8 @@ import { useWalletPayment } from '@/wallets/client/hooks'
|
||||
this is just like useMutation with a few changes:
|
||||
1. pays an invoice returned by the mutation
|
||||
2. takes an onPaid and onPayError callback, and additional options for payment behavior
|
||||
- persistOnNavigate which will keep the invoice in the cache after navigation
|
||||
- namely forceWaitForPayment which will always wait for the invoice to be paid
|
||||
- and persistOnNavigate which will keep the invoice in the cache after navigation
|
||||
3. onCompleted behaves a little differently, but analogously to useMutation, ie clientside side effects
|
||||
of completion can still rely on it
|
||||
a. it's called before the invoice is paid for optimistic updates
|
||||
@ -76,7 +77,7 @@ export function usePaidMutation (mutation,
|
||||
|
||||
// use the most inner callbacks/options if they exist
|
||||
const {
|
||||
onPaid, onPayError, persistOnNavigate,
|
||||
onPaid, onPayError, forceWaitForPayment, persistOnNavigate,
|
||||
update, waitFor = inv => inv?.actionState === 'PAID', updateOnFallback
|
||||
} = { ...options, ...innerOptions }
|
||||
const ourOnCompleted = innerOnCompleted || onCompleted
|
||||
@ -106,7 +107,7 @@ export function usePaidMutation (mutation,
|
||||
})
|
||||
|
||||
// should we wait for the invoice to be paid?
|
||||
if (response?.paymentMethod === 'OPTIMISTIC') {
|
||||
if (response?.paymentMethod === 'OPTIMISTIC' && !forceWaitForPayment) {
|
||||
// onCompleted is called before the invoice is paid for optimistic updates
|
||||
ourOnCompleted?.(data)
|
||||
// don't wait to pay the invoice
|
||||
@ -206,8 +207,7 @@ export const paidActionCacheMods = {
|
||||
fields: {
|
||||
actionState: () => 'PAID',
|
||||
confirmedAt: () => new Date().toISOString(),
|
||||
satsReceived: () => invoice.satsRequested,
|
||||
hmac: () => invoice.hmac
|
||||
satsReceived: () => invoice.satsRequested
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
32
components/use-visibility.js
Normal file
32
components/use-visibility.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// observe the passed element ref and return its visibility
|
||||
export default function useVisibility (elementRef, options = {}) {
|
||||
// threshold is the percentage of the element that must be visible to be considered visible
|
||||
// with pastElement, we consider the element not visible only when we're past it
|
||||
const { threshold = 0, pastElement = false } = options
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current
|
||||
if (!element || !window.IntersectionObserver || typeof window === 'undefined') return
|
||||
|
||||
const observer = new window.IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true)
|
||||
} else if (pastElement) {
|
||||
setIsVisible(entry.boundingClientRect.top > 0)
|
||||
} else {
|
||||
setIsVisible(false)
|
||||
}
|
||||
}, { threshold }
|
||||
)
|
||||
|
||||
// observe the passed element ref
|
||||
observer.observe(element)
|
||||
return () => observer.disconnect()
|
||||
}, [threshold, elementRef, pastElement])
|
||||
|
||||
return isVisible
|
||||
}
|
||||
@ -15,4 +15,3 @@ Scroogey
|
||||
SimpleStacker
|
||||
klk
|
||||
brymut
|
||||
abhishandy
|
||||
@ -530,9 +530,8 @@ services:
|
||||
- '--bitcoin-rpcuser=${RPC_USER}'
|
||||
- '--bitcoin-rpcpassword=${RPC_PASS}'
|
||||
- '--large-channels'
|
||||
- '--clnrest-port=3010'
|
||||
- '--clnrest-host=0.0.0.0'
|
||||
- '--clnrest-protocol=http'
|
||||
- '--rest-port=3010'
|
||||
- '--rest-host=0.0.0.0'
|
||||
expose:
|
||||
- "9735"
|
||||
ports:
|
||||
@ -843,27 +842,6 @@ services:
|
||||
CONNECT: "localhost:${LNBITS_WEB_PORT_V1}"
|
||||
TORDIR: "/app/.tor"
|
||||
cpu_shares: "${CPU_SHARES_LOW}"
|
||||
lnpub:
|
||||
image: ghcr.io/shocknet/lightning-pub@sha256:cd7bb9298d09a2cdaf1b6456ef6154e3ba24f7b902ad29cda2c08c2a4fa2af6e
|
||||
container_name: lnpub
|
||||
profiles:
|
||||
- wallets
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- lnpub:/app/data
|
||||
- lnd:/app/.lnd
|
||||
environment:
|
||||
- LND_ADDRESS=lnd:10009
|
||||
- LND_CERT_PATH=/app/.lnd/tls.cert
|
||||
- LND_MACAROON_PATH=/app/.lnd/data/chain/bitcoin/regtest/admin.macaroon
|
||||
ports:
|
||||
- ${LNPUB_PORT_1776:-1776}:1776
|
||||
- ${LNPUB_PORT_1777:-1777}:1777
|
||||
depends_on:
|
||||
lnd:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
cpu_shares: "${CPU_SHARES_LOW}"
|
||||
dnsmasq:
|
||||
image: 4km3/dnsmasq:2.90-r3
|
||||
profiles:
|
||||
@ -900,7 +878,6 @@ volumes:
|
||||
tordata:
|
||||
eclair:
|
||||
dnsmasq:
|
||||
lnpub:
|
||||
|
||||
networks:
|
||||
default: {}
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
FROM polarlightning/clightning:24.11
|
||||
FROM polarlightning/clightning:23.08
|
||||
|
||||
# make sure that wallet and identity is persisted across rebuilds
|
||||
# https://docs.corelightning.org/docs/grpc#generating-custom-certificates-optional
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y jq wget \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN wget https://raw.githubusercontent.com/ElementsProject/lightning/v23.08/plugins/clnrest/requirements.txt \
|
||||
&& pip install -r requirements.txt
|
||||
|
||||
# make sure that wallet and identity is persisted across rebuilds.
|
||||
# server certificates contain stacker_lnd as a custom domain.
|
||||
# see https://docs.corelightning.org/docs/grpc#generating-custom-certificates-optional
|
||||
# since CLNRest in CLNv23.08 seems to use client certificates, they also contain stacker_lnd as a custom domain.
|
||||
# see https://github.com/ElementsProject/lightning/tree/v23.08/plugins/clnrest#configuration
|
||||
COPY ["./hsm_secret", "./ca-key.pem", "./ca.pem", "./server-key.pem", "./server.pem", "./client-key.pem", "./client.pem", "/home/clightning/.lightning/regtest/"]
|
||||
@ -1 +1 @@
|
||||
70A2D30FE991B24B5A6BF85421BE5EF083665E84
|
||||
70A2D30FE991B24B5A6BF85421BE5EF083665E81
|
||||
|
||||
@ -1,28 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7LKm+zhlA/q9K
|
||||
tlmjLsLCSSFla0SsJmde1BQfnIZvjl093Rt1pVpRCENWjYfDub6f1ceLmWpi9fCF
|
||||
PRPwRUpE/NATIWJlSO7/DHBsXRVMQiM7E2hRe16/z3OK7QV93cIPnNMq+JCeKYby
|
||||
I8qh+bLV61w611iCGaf2EjKmtHazKES3qGeO4/8IeLAv+Y7zsSfsErz8J8lOuM+M
|
||||
pMIl7ulSEK4DILJ2lc/f0/Com5JVjpe/tm4u+GuM67NKDt6WtNA5qTn+JZDWSOJ3
|
||||
rtZ/+FfleAf5VH4eJ/TZKSeA0PlzSl06Suvu+R+MKT0llHl9yDfk7Afg/prABykC
|
||||
3OoeNLxBAgMBAAECggEAAukH3qKfu+X53cSGEkZ42sJ+VXBcifyy4MOVaIRrhrKE
|
||||
+qBEfAjNJbNmMKNUuBNcRmnxh1ckU1OVoMy5UaQSKo5vwcxkFkUTCj4sRVMRMLVa
|
||||
jOGZXqL3by4PktpqmnFnQgzjL6jbvsnQglVSIkCaqj7VmUid5K/3b5kK1pK5wcFY
|
||||
tYgGsyBKLeiilIxMG5btXZhevnaoKSNo38Msy9rOx7WnOurw5rOmbjwjd2DxsiYl
|
||||
I5+FBBZ+oTwhob3BETQK8NhtUHAD2dFPZ0THjjU9z7n+U5C1mxpqbEytJuBllZ7g
|
||||
7QEsYb9a8cJ4PlJUFD9XK+9Wx7vaz+y/3+EJctY2EQKBgQDqQwGyhwCRyND0CSqn
|
||||
n98rf5hgxMfdzAkTP9KUVarL8JA52lR9wNUSTecuaFkduTWel+gOb23dVpy9OOqt
|
||||
4Uhc4ORNh/b6cnAomVfWK0gmPmBHzKBgL5On7E+HrM4j2i4HQuOFZY1sWUznWHcK
|
||||
Gzp1HS33NQ1+Bqa8kK0dPT/7bQKBgQDMixOude2men0noXAGwlnpMwNx3RVmaWum
|
||||
Zm8OJsmbnjCULEWYCMU6tbUPwAfmpO3g2EDF2EjOF3exg5qQ8G8tiKk/ptZVnZaZ
|
||||
EEm6/tHafC6ZWGh0ZqkFJVnjROzcoMhJ79eaDuNcfyErpceqh6FAUwuDtxZoZhRa
|
||||
MyasgJILpQKBgQDFuDxshAl1AjtqXXpE9GiV/CGOO0g96YIXXxzK2etIKghw69e4
|
||||
M9MimeHvh17/+VxKOQhBdscRs5KjGrDohWZgDehjj8hi5cfIXHSIbQt+S9NThmAu
|
||||
DrnfV1JgvvdVx0ZmFoyWAHp24oBIGqCORSD3y+MJ7RswLUj5ilqyfQnNmQKBgCD5
|
||||
Rqn4PuB4nJZ38vxT9nSeka6YamvBEOaZWsvYOuYIYWJxpKJ2v3zQcCji1yM6cVKu
|
||||
6fo6/LmklNocEh3NdM7NWiN1vNW+etmgA5LOo3vqSwTTeLtTFWpL9CqsINcMYF1y
|
||||
+bnPLp3prKpBpmd6R+d7u753FHiuBBfqaRCkBWc1AoGBANZY0EBBkEcdodTjT4AP
|
||||
22BRZwg9duUa6CObeNNjKTUhX/ClVXQDJRG0jEwz4fT5wWMBKZ4NZhaMGIhNciKg
|
||||
OdeU2Y3tGHXemvJaNiYg88mkXZzlNeN5CRR5BASDfvP9Chs7/48ZCfgQMn25sbB6
|
||||
sXJd+BDdOkuQADolHWjseeqp
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDJM3OX9henwgjZ
|
||||
vBEGmnDEDuG5XXEAzYTvd3Jz8eoexkTXnXRAJdngm0gVsFBFwbGOrn4rJH1Nc5WA
|
||||
xE6Z+ZIW4DjvQi8hhPQHZpWesuRrBXCdF+3dUWdCYHGSA/c+ndcQx7WZkZpCpOaT
|
||||
X8B2MhfdgfuoEhuGgL8oCauv/55oDsbQZliCti2SpXDt7azOenM7WiJbzMWemaJa
|
||||
z3WbOsh2QZELfXWS1KmpN1jBmN7ImcbNV7IX/8zK0mZ2YvOQBY5H06m8q3lXtSFY
|
||||
tMsZQuwYDNhpKXw9e6ey1GJWNZ4s8UhUG3OeLQuvL7o8XWW2oA9LplJF3dsy10cS
|
||||
Mb/vrO3JAgMBAAECggEAEaAFN0GijtqLhe9Ymnh6+mHHWkKaPJTEWWngR9HgTXPW
|
||||
4gB0B46JSIixxtycG5A9Kp+Ug9j9dQR0C0SnEgvvxTs9IZBtqoUID4HoB2/qXIms
|
||||
dMZ82s9fuUvThxP726teBKVJ/jroBYCGhlBz/qaiuo/BaNa1PJIYrLw0IwXEbZ79
|
||||
+LqtdoUTOb+ZzI04aZR82OSrOGbz7cE8uM8uNAM+xrbM11L1IUGLoLZoUyvJww4v
|
||||
F5ZDWBe8+3D50ypCdmrL805YW1scySnA3YNLyXlfl6jE0AeYd+ZOVGhH4MfkdwXF
|
||||
O/yGTF3UEhTAdtryXQw7JiLA0qTakkLQeQ8ydLZzgQKBgQDqppULwJADJsnI5b3x
|
||||
HOklQEcmg7ySMLkVmyDEL7vcesHnsUNjCWwqSF4nR4eQJcM3zfvkjZN4sgUw8aCm
|
||||
W3G+v4mFA24xEVcuoA9EXUZuMc6XRVchV/EdgIbVCNH9+ATKkFMiaCQu11QmjkR+
|
||||
da4+a6bo+L6Zl9ifVoJTb/0jeQKBgQDbgcSJwDUo6b9n0Dkkwpsj6fj7Wa7jLW9a
|
||||
8Bz3pJj5AUUWjTQ17tLl7WY8EFYGlUjie6n5lM8vQZrPCnw4ZUKBAIoxIiRwQt1j
|
||||
tFBjP+ztJ2AUER6hBF0uFUqNNdrlryOBegnxelfb//9+SyzICjRLChiyUxEzSVSY
|
||||
T2Maz6S40QKBgHBABjbcBEhtqsPfG3EXanS2fhLvnCq3AiNS1WbkitLbKp1ikCD1
|
||||
ZfgILHpP3orXdb7hW+mmzHBFgPQ78qUCQ7SDPg8SaAkzCWi1ivgiQsn+K5zzv82k
|
||||
mySI0ndgw8vhdLFOP2bLONvriEb1cdCpDRSxPORf8hXZrPf0U14EyazJAoGAKPkC
|
||||
/5dyFM303WLfl73/iWeeAwTNgTg05euV7J+7shcLl1cKHNsUYLi8cY+3DwmEjkn5
|
||||
A05EkhST2fuiDkDQdhXstZki5hWFD5xTuQLwrZ/A7l33sqSG5BgzT0JzNpZHcV6f
|
||||
RoTIq5cQULmlhT1qX2tmCrs4pbMVaEzBOfryS1ECgYBgY+XR3ggvagkM/5jSeCT4
|
||||
45M0UChNAFWcSVUyaDSWlu5ec/hJ7hgUsA7Wz9j8upSQZf5fKVt/k+J45WHdTNyH
|
||||
HTcO37iFjNwRLnIEdiBoQSBEkTBChkcmxpSoMGryZEu1Ng93WIxppvlLzKeviidf
|
||||
HVqTsNNjnv2JqMKD6rxDlw==
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICkTCCAXkCAQAwGjEYMBYGA1UEAwwPY2xuIHJlc3Qgc2VydmVyMIIBIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuyypvs4ZQP6vSrZZoy7CwkkhZWtErCZn
|
||||
XtQUH5yGb45dPd0bdaVaUQhDVo2Hw7m+n9XHi5lqYvXwhT0T8EVKRPzQEyFiZUju
|
||||
/wxwbF0VTEIjOxNoUXtev89ziu0Ffd3CD5zTKviQnimG8iPKofmy1etcOtdYghmn
|
||||
9hIyprR2syhEt6hnjuP/CHiwL/mO87En7BK8/CfJTrjPjKTCJe7pUhCuAyCydpXP
|
||||
39PwqJuSVY6Xv7ZuLvhrjOuzSg7elrTQOak5/iWQ1kjid67Wf/hX5XgH+VR+Hif0
|
||||
2SkngND5c0pdOkrr7vkfjCk9JZR5fcg35OwH4P6awAcpAtzqHjS8QQIDAQABoDIw
|
||||
MAYJKoZIhvcNAQkOMSMwITAfBgNVHREEGDAWhwR/AAABgglsb2NhbGhvc3SCA2Ns
|
||||
bjANBgkqhkiG9w0BAQsFAAOCAQEAjNRTLpPVllB0talokK7HalVAfs/SBL4dUAAB
|
||||
aCpe9Q8MNlSBORgedRwfzecxpNtJ+Tb5m9nIw6vfnfRZ6mxKLW7wPyOG6t83Pivc
|
||||
v30rPV/wbW2DvPe8dj8K2Fyh8lWWjJCMSar1ZvFsImwXdEzCOdEU3wUYOPS+hT7+
|
||||
6walQ/L2jbWtmfUibbXECoekqg/9WSpVik4iq9oBbJF6V6gk2VKskioDTX3eAC07
|
||||
6/P59CZJdE6vp5kjR868jt5VxTDa5MdJ8d2QFPJqZ9TmduZ36camALXbMbbB+e2+
|
||||
u1/ZX6DtGOnqkBWBbi3IVZeZks4lzUeXYp3/BzNeCp5F2Xfq0Q==
|
||||
MIICmDCCAYACAQAwUzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
|
||||
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEMMAoGA1UEAwwDY2xu
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyTNzl/YXp8II2bwRBppw
|
||||
xA7huV1xAM2E73dyc/HqHsZE1510QCXZ4JtIFbBQRcGxjq5+KyR9TXOVgMROmfmS
|
||||
FuA470IvIYT0B2aVnrLkawVwnRft3VFnQmBxkgP3Pp3XEMe1mZGaQqTmk1/AdjIX
|
||||
3YH7qBIbhoC/KAmrr/+eaA7G0GZYgrYtkqVw7e2sznpzO1oiW8zFnpmiWs91mzrI
|
||||
dkGRC311ktSpqTdYwZjeyJnGzVeyF//MytJmdmLzkAWOR9OpvKt5V7UhWLTLGULs
|
||||
GAzYaSl8PXunstRiVjWeLPFIVBtzni0Lry+6PF1ltqAPS6ZSRd3bMtdHEjG/76zt
|
||||
yQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAIHrxnnPdlZL+ODOIajoqyGWxNju
|
||||
Tigv0sKW6T+ty6sSJaHlHJynGAKKXRz8NzyUpeAwaBXTSU9Ca3PSCs4vvAvvamGT
|
||||
nqEUeb9YhLe4pkBBUtpQrml6ixotids1PFf38f6965Z34S3xivucBRrGnB+hGFEz
|
||||
QKgMnQmUan9AWOqAQEzWU4sVkNzYtH1sjtRLSIVyoWD/8DiBlEL0poKq6/NjnT1z
|
||||
Q6kZh5uGbeViZ9VzCM1xJVpVP1Z1oVZkMHndKf4I03IRWhW34ddhQJcja9nr2wdU
|
||||
m1Pv78262l5B0YdXTd3C73i70k7GqSusKCW+HvQAy8DmEDRxJRLGTpYNsrc=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICYDCCAgagAwIBAgIUcKLTD+mRsktaa/hUIb5e8INmXoQwCgYIKoZIzj0EAwIw
|
||||
FjEUMBIGA1UEAwwLY2xuIFJvb3QgQ0EwHhcNMjUwOTAxMjMzOTE5WhcNMjYwOTAx
|
||||
MjMzOTE5WjAaMRgwFgYDVQQDDA9jbG4gcmVzdCBzZXJ2ZXIwggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQC7LKm+zhlA/q9KtlmjLsLCSSFla0SsJmde1BQf
|
||||
nIZvjl093Rt1pVpRCENWjYfDub6f1ceLmWpi9fCFPRPwRUpE/NATIWJlSO7/DHBs
|
||||
XRVMQiM7E2hRe16/z3OK7QV93cIPnNMq+JCeKYbyI8qh+bLV61w611iCGaf2EjKm
|
||||
tHazKES3qGeO4/8IeLAv+Y7zsSfsErz8J8lOuM+MpMIl7ulSEK4DILJ2lc/f0/Co
|
||||
m5JVjpe/tm4u+GuM67NKDt6WtNA5qTn+JZDWSOJ3rtZ/+FfleAf5VH4eJ/TZKSeA
|
||||
0PlzSl06Suvu+R+MKT0llHl9yDfk7Afg/prABykC3OoeNLxBAgMBAAGjYzBhMB8G
|
||||
A1UdEQQYMBaHBH8AAAGCCWxvY2FsaG9zdIIDY2xuMB0GA1UdDgQWBBS2TbrQ7FiH
|
||||
i01mjkx67gh+nBwX8zAfBgNVHSMEGDAWgBSEcmN/9rzS2hR6G7EIgsX+51N0CjAK
|
||||
BggqhkjOPQQDAgNIADBFAiAA2ffEzwtVk5VnHbO00VWaEM3G08M8T0WwUmT1Vvy3
|
||||
IAIhALOJl3d4R/PXiGcw8vRse8iRi+TmN3ZXB0QEfubSsfBP
|
||||
MIICLzCCAdUCFHCi0w/pkbJLWmv4VCG+XvCDZl6BMAoGCCqGSM49BAMCMBYxFDAS
|
||||
BgNVBAMMC2NsbiBSb290IENBMB4XDTI0MTEyMDE4MDgxMVoXDTI1MTEyMDE4MDgx
|
||||
MVowUzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEMMAoGA1UEAwwDY2xuMIIBIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyTNzl/YXp8II2bwRBppwxA7huV1xAM2E
|
||||
73dyc/HqHsZE1510QCXZ4JtIFbBQRcGxjq5+KyR9TXOVgMROmfmSFuA470IvIYT0
|
||||
B2aVnrLkawVwnRft3VFnQmBxkgP3Pp3XEMe1mZGaQqTmk1/AdjIX3YH7qBIbhoC/
|
||||
KAmrr/+eaA7G0GZYgrYtkqVw7e2sznpzO1oiW8zFnpmiWs91mzrIdkGRC311ktSp
|
||||
qTdYwZjeyJnGzVeyF//MytJmdmLzkAWOR9OpvKt5V7UhWLTLGULsGAzYaSl8PXun
|
||||
stRiVjWeLPFIVBtzni0Lry+6PF1ltqAPS6ZSRd3bMtdHEjG/76ztyQIDAQABMAoG
|
||||
CCqGSM49BAMCA0gAMEUCIDExbaYoitsXHdu8wgVlv7LozbNK9Te6t292ctH0dZCy
|
||||
AiEAjg/GqpNIv01ACeK/5+HVLtba8TL6vBd9dENR4ADDoj4=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
[req]
|
||||
default_bits = 2048
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = req_ext
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
CN = "cln rest server"
|
||||
|
||||
[req_ext]
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
DNS.1 = localhost
|
||||
DNS.2 = cln
|
||||
@ -1,16 +1,16 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICkTCCAXkCAQAwGjEYMBYGA1UEAwwPY2xuIHJlc3Qgc2VydmVyMIIBIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt+KCYNwj+YJNQXlJwuFiMvJK5nU1DOcp
|
||||
9dvo4gvBClzwOeaRPsB4qnkWLxZqQ8+/q+LMD7P+QUXHqI49b8SFm7FFEpCL5GEN
|
||||
090F1+GIEUBgEqMPHP3QRfyhnI4hmo0fgGArI7BMhBZrYH7nRKSNoEgpWd8dtm9N
|
||||
R/jx7j9sWXtFx6LZoM6yZZXVy6DdDGNk7WR0o98fR3NtT+OzUAkCMJR45aT7SYUN
|
||||
adCVNcFKsJqVMq0HDoa4sW1u184Fg5Bx38+gDXGn4VsDXwVXNMNxmGNY7ADn+AiR
|
||||
q0MLGDu6B5a+Raqt+LAlvvsqbFww8wtWUHQ2gnvXZBEVS+Wi2U7VpQIDAQABoDIw
|
||||
MAYJKoZIhvcNAQkOMSMwITAfBgNVHREEGDAWhwR/AAABgglsb2NhbGhvc3SCA2Ns
|
||||
bjANBgkqhkiG9w0BAQsFAAOCAQEAWSvrwxJTRyK9mDwft4qsCCStS3Gunh7OSGeG
|
||||
da+Lcrk4NABr3SnWwl/k3HEm2ZNt4/k0eDB95ElvUUcXoC5ptlyuVup97GZyO2VG
|
||||
SqLcMlL3/vuGpkV6LGAbiLbcKKqBWwV5mP+bThMRfLJdiUpjoTrjiaHElfu8Be4e
|
||||
qKzHYKbmSn7I7eDixsSsySPVL8h+YIamMH68BNi2AEJJH+Yyd/J7qZbP0zu9X2lQ
|
||||
I09gdw49FEjx38Se8Jol7GRdzJnqFElnRHKXpR+FwtmsFMh+4Syc9Zz1fgOuBA6B
|
||||
HR+OGRqO2jT+aINPB0x+GgKWvjt/DXxvF0tq42Q8/P4i9in95w==
|
||||
MIICmDCCAYACAQAwUzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
|
||||
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEMMAoGA1UEAwwDY2xu
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt+KCYNwj+YJNQXlJwuFi
|
||||
MvJK5nU1DOcp9dvo4gvBClzwOeaRPsB4qnkWLxZqQ8+/q+LMD7P+QUXHqI49b8SF
|
||||
m7FFEpCL5GEN090F1+GIEUBgEqMPHP3QRfyhnI4hmo0fgGArI7BMhBZrYH7nRKSN
|
||||
oEgpWd8dtm9NR/jx7j9sWXtFx6LZoM6yZZXVy6DdDGNk7WR0o98fR3NtT+OzUAkC
|
||||
MJR45aT7SYUNadCVNcFKsJqVMq0HDoa4sW1u184Fg5Bx38+gDXGn4VsDXwVXNMNx
|
||||
mGNY7ADn+AiRq0MLGDu6B5a+Raqt+LAlvvsqbFww8wtWUHQ2gnvXZBEVS+Wi2U7V
|
||||
pQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAD1yimwCgeFZwkSyzFYrzf9e68L8
|
||||
Or8Fo7w5Wa1dtiGLz8hyHL5hephUgR5xA92VJ6zIXFXWYMfOvEFvRPCmXMjnS1Y5
|
||||
hxfLo+6NJ+U4qX301Dd3NEKCDqxYQIoRVxY+fqZBYn7vbVwimmdf3Epn8CX/tvSq
|
||||
49YWkq2yRDUrcOzvvt6wfG6Gv1N2Igra2op3A0LywLXkPYNFmvMOvXzA9/Tk5dGb
|
||||
LNH1aIYbgMgA9XL7g1XVHqOqIBjqTukwVUJ6o/MVx8T9eoqZQRfV1s92BkhfnugS
|
||||
XQ16AO45gXNnjz0T1OjXLHzYpXIlSneo1BMIh9/eAi3d5PihBk2ss8eo3U4=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICXzCCAgagAwIBAgIUcKLTD+mRsktaa/hUIb5e8INmXoIwCgYIKoZIzj0EAwIw
|
||||
FjEUMBIGA1UEAwwLY2xuIFJvb3QgQ0EwHhcNMjUwOTAxMjMyNDMxWhcNMjYwOTAx
|
||||
MjMyNDMxWjAaMRgwFgYDVQQDDA9jbG4gcmVzdCBzZXJ2ZXIwggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQC34oJg3CP5gk1BeUnC4WIy8krmdTUM5yn12+ji
|
||||
C8EKXPA55pE+wHiqeRYvFmpDz7+r4swPs/5BRceojj1vxIWbsUUSkIvkYQ3T3QXX
|
||||
4YgRQGASow8c/dBF/KGcjiGajR+AYCsjsEyEFmtgfudEpI2gSClZ3x22b01H+PHu
|
||||
P2xZe0XHotmgzrJlldXLoN0MY2TtZHSj3x9Hc21P47NQCQIwlHjlpPtJhQ1p0JU1
|
||||
wUqwmpUyrQcOhrixbW7XzgWDkHHfz6ANcafhWwNfBVc0w3GYY1jsAOf4CJGrQwsY
|
||||
O7oHlr5Fqq34sCW++ypsXDDzC1ZQdDaCe9dkERVL5aLZTtWlAgMBAAGjYzBhMB8G
|
||||
A1UdEQQYMBaHBH8AAAGCCWxvY2FsaG9zdIIDY2xuMB0GA1UdDgQWBBRJm0s9JXB+
|
||||
zXHKjB4oTDS31a+h2zAfBgNVHSMEGDAWgBSEcmN/9rzS2hR6G7EIgsX+51N0CjAK
|
||||
BggqhkjOPQQDAgNHADBEAiBfwGNj4RZBTmIb44Nk1nTD/r7fIkEeDgkgxkOiAiQ9
|
||||
twIgK+vJ3dxP21cH5ye2ODbMqe0RC/3FoyJegqIUocvTu6s=
|
||||
MIICLzCCAdUCFHCi0w/pkbJLWmv4VCG+XvCDZl6AMAoGCCqGSM49BAMCMBYxFDAS
|
||||
BgNVBAMMC2NsbiBSb290IENBMB4XDTI0MTEyMDE4MDE0NloXDTI1MTEyMDE4MDE0
|
||||
NlowUzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEMMAoGA1UEAwwDY2xuMIIBIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt+KCYNwj+YJNQXlJwuFiMvJK5nU1DOcp
|
||||
9dvo4gvBClzwOeaRPsB4qnkWLxZqQ8+/q+LMD7P+QUXHqI49b8SFm7FFEpCL5GEN
|
||||
090F1+GIEUBgEqMPHP3QRfyhnI4hmo0fgGArI7BMhBZrYH7nRKSNoEgpWd8dtm9N
|
||||
R/jx7j9sWXtFx6LZoM6yZZXVy6DdDGNk7WR0o98fR3NtT+OzUAkCMJR45aT7SYUN
|
||||
adCVNcFKsJqVMq0HDoa4sW1u184Fg5Bx38+gDXGn4VsDXwVXNMNxmGNY7ADn+AiR
|
||||
q0MLGDu6B5a+Raqt+LAlvvsqbFww8wtWUHQ2gnvXZBEVS+Wi2U7VpQIDAQABMAoG
|
||||
CCqGSM49BAMCA0gAMEUCIDcuZHxYHgEr0PHIR6JJF72T7c1JccvIvjl0JIqjjwwq
|
||||
AiEAtzbdnTMuJP16csHt+RrSsIVGUy5G5byI/M0RtIwyQGQ=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -8,7 +8,7 @@ sub: meta
|
||||
|
||||
_To quickly browse through this FAQ page, click the chapters icon in the top-right corner. This will let you scroll through all chapters or search for a particular topic within this page._
|
||||
|
||||
last updated: August 13, 2025
|
||||
last updated: July 16, 2025
|
||||
|
||||
---
|
||||
|
||||
@ -46,7 +46,7 @@ See the [section about territories](#territories) for details.
|
||||
|
||||
### Do I need bitcoin to use Stacker News?
|
||||
|
||||
No. You don't need bitcoin to get started. You can create your bio for free (with limited visibility) and earn Cowboy Credits (CCs) from zaps to cover fees. Posts and comments incur a small fee to prevent spam and to encourage quality contributions. Many stackers earn enough from their posts and comments to continue posting on the site indefinitely without ever buying CCs with sats.
|
||||
No. Every new stacker can post or comment for free (with limited visibility) while they earn their first few CCs or sats. After a stacker has gained a balance, subsequent posts and comments will incur a small fee to prevent spam and to encourage quality contributions. Many stackers earn enough from their posts and comments to continue posting on the site indefinitely without ever buying CCs with sats.
|
||||
|
||||
[Post and comment fees vary depending on the territory](#why-does-it-cost-more-to-post-in-some-territories).
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ export const COMMENT_FIELDS = gql`
|
||||
otsHash
|
||||
ncomments
|
||||
nDirectComments
|
||||
live @client
|
||||
injected @client
|
||||
imgproxyUrls
|
||||
rel
|
||||
apiKey
|
||||
@ -55,7 +55,6 @@ export const COMMENT_FIELDS = gql`
|
||||
id
|
||||
actionState
|
||||
confirmedAt
|
||||
hmac
|
||||
}
|
||||
cost
|
||||
}
|
||||
@ -95,7 +94,6 @@ export const COMMENT_FIELDS_NO_CHILD_COMMENTS = gql`
|
||||
commentCredits
|
||||
mine
|
||||
otsHash
|
||||
live @client
|
||||
imgproxyUrls
|
||||
rel
|
||||
apiKey
|
||||
@ -103,7 +101,6 @@ export const COMMENT_FIELDS_NO_CHILD_COMMENTS = gql`
|
||||
id
|
||||
actionState
|
||||
confirmedAt
|
||||
hmac
|
||||
}
|
||||
cost
|
||||
}
|
||||
@ -155,16 +152,6 @@ export const COMMENTS = gql`
|
||||
comments {
|
||||
comments {
|
||||
...CommentFields
|
||||
comments {
|
||||
comments {
|
||||
...CommentFields
|
||||
comments {
|
||||
comments {
|
||||
...CommentFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,19 +164,48 @@ export const COMMENTS = gql`
|
||||
}
|
||||
}`
|
||||
|
||||
export const HAS_COMMENTS = gql`
|
||||
fragment HasComments on Item {
|
||||
comments
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_NEW_COMMENTS = gql`
|
||||
${COMMENT_FIELDS_NO_CHILD_COMMENTS}
|
||||
|
||||
query GetNewComments($itemId: ID, $after: Date) {
|
||||
newComments(itemId: $itemId, after: $after) {
|
||||
export const COMMENT_WITH_NEW_RECURSIVE = gql`
|
||||
${COMMENT_FIELDS}
|
||||
${COMMENTS}
|
||||
|
||||
fragment CommentWithNewRecursive on Item {
|
||||
...CommentFields
|
||||
comments {
|
||||
comments {
|
||||
...CommentFieldsNoChildComments
|
||||
...CommentsRecursive
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const COMMENT_WITH_NEW_LIMITED = gql`
|
||||
${COMMENT_FIELDS}
|
||||
|
||||
fragment CommentWithNewLimited on Item {
|
||||
...CommentFields
|
||||
comments {
|
||||
comments {
|
||||
...CommentFields
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const COMMENT_WITH_NEW_MINIMAL = gql`
|
||||
${COMMENT_FIELDS}
|
||||
|
||||
fragment CommentWithNewMinimal on Item {
|
||||
...CommentFields
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_NEW_COMMENTS = gql`
|
||||
${COMMENTS}
|
||||
|
||||
query GetNewComments($rootId: ID, $after: Date) {
|
||||
newComments(rootId: $rootId, after: $after) {
|
||||
comments {
|
||||
...CommentsRecursive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +81,6 @@ export const ITEM_FIELDS = gql`
|
||||
confirmedAt
|
||||
}
|
||||
cost
|
||||
meCommentsViewedAt
|
||||
}`
|
||||
|
||||
export const ITEM_FULL_FIELDS = gql`
|
||||
@ -92,15 +91,12 @@ export const ITEM_FULL_FIELDS = gql`
|
||||
text
|
||||
root {
|
||||
id
|
||||
createdAt
|
||||
title
|
||||
bounty
|
||||
bountyPaidTo
|
||||
subName
|
||||
mine
|
||||
ncomments
|
||||
lastCommentAt
|
||||
meCommentsViewedAt
|
||||
user {
|
||||
id
|
||||
name
|
||||
@ -215,9 +211,3 @@ export const RELATED_ITEMS_WITH_ITEM = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_ITEM_USER_VIEW = gql`
|
||||
mutation updateCommentsViewAt($id: ID!, $meCommentsViewedAt: Date!) {
|
||||
updateCommentsViewAt(id: $id, meCommentsViewedAt: $meCommentsViewedAt)
|
||||
}
|
||||
`
|
||||
|
||||
@ -131,6 +131,11 @@ export const SET_SETTINGS = gql`
|
||||
}
|
||||
}`
|
||||
|
||||
export const DELETE_WALLET = gql`
|
||||
mutation removeWallet {
|
||||
removeWallet
|
||||
}`
|
||||
|
||||
export const NAME_QUERY = gql`
|
||||
query nameAvailable($name: String!) {
|
||||
nameAvailable(name: $name)
|
||||
|
||||
@ -324,9 +324,9 @@ function getClient (uri) {
|
||||
}
|
||||
}
|
||||
},
|
||||
live: {
|
||||
read (live) {
|
||||
return live || false
|
||||
injected: {
|
||||
read (injected) {
|
||||
return injected || false
|
||||
}
|
||||
},
|
||||
meAnonSats: {
|
||||
|
||||
@ -196,10 +196,11 @@ export async function multiAuthMiddleware (req, res) {
|
||||
}
|
||||
|
||||
const ok = await checkMultiAuthCookies(req, res)
|
||||
if (ok) {
|
||||
await refreshMultiAuthCookies(req, res)
|
||||
} else {
|
||||
if (!ok) {
|
||||
await resetMultiAuthCookies(req, res)
|
||||
return switchSessionCookie(req)
|
||||
}
|
||||
|
||||
await refreshMultiAuthCookies(req, res)
|
||||
return switchSessionCookie(req)
|
||||
}
|
||||
|
||||
50
lib/cln.js
50
lib/cln.js
@ -1,8 +1,8 @@
|
||||
import crossFetch from 'cross-fetch'
|
||||
import fetch from 'cross-fetch'
|
||||
import crypto from 'crypto'
|
||||
import { getAgent } from '@/lib/proxy'
|
||||
import { assertContentTypeJson, assertResponseOk } from './url'
|
||||
import { fetchWithTimeout, FetchTimeoutError } from './fetch'
|
||||
import { FetchTimeoutError } from './fetch'
|
||||
import { WALLET_CREATE_INVOICE_TIMEOUT_MS } from './constants'
|
||||
|
||||
export const createInvoice = async ({ msats, description, expiry }, { socket, rune, cert }, { signal }) => {
|
||||
@ -13,9 +13,15 @@ export const createInvoice = async ({ msats, description, expiry }, { socket, ru
|
||||
const method = 'POST'
|
||||
let res
|
||||
try {
|
||||
res = await crossFetch(url, {
|
||||
res = await fetch(url, {
|
||||
method,
|
||||
headers: headers(rune),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Rune: rune,
|
||||
// can be any node id, only required for CLN v23.08 and below
|
||||
// see https://docs.corelightning.org/docs/rest#server
|
||||
nodeId: '02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490'
|
||||
},
|
||||
agent,
|
||||
body: JSON.stringify({
|
||||
// CLN requires a unique label for every invoice
|
||||
@ -46,42 +52,6 @@ export const createInvoice = async ({ msats, description, expiry }, { socket, ru
|
||||
return inv
|
||||
}
|
||||
|
||||
export const sendPayment = async (bolt11, { socket, rune }, { signal }) => {
|
||||
// XXX we don't ask for the CA certificate because the browser's fetch API doesn't support http agents to override it.
|
||||
// Therefore, CLNRest send will only work with common CA certificates.
|
||||
|
||||
// API documentation
|
||||
// https://docs.corelightning.org/reference/pay
|
||||
const url = new URL(
|
||||
'/v1/pay',
|
||||
process.env.NODE_ENV === 'development' ? `http://${socket}` : `https://${socket}`)
|
||||
const method = 'POST'
|
||||
const res = await fetchWithTimeout(url, {
|
||||
method,
|
||||
headers: headers(rune),
|
||||
body: JSON.stringify({ bolt11 }),
|
||||
signal
|
||||
})
|
||||
assertResponseOk(res, { method })
|
||||
assertContentTypeJson(res, { method })
|
||||
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message)
|
||||
}
|
||||
return result.payment_preimage
|
||||
}
|
||||
|
||||
function headers (rune) {
|
||||
const headers = new Headers()
|
||||
headers.append('Content-Type', 'application/json')
|
||||
headers.append('Rune', rune)
|
||||
// can be any node id, only required for CLN v23.08 and below
|
||||
// see https://docs.corelightning.org/docs/rest#server
|
||||
headers.append('nodeId', '02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490')
|
||||
return headers
|
||||
}
|
||||
|
||||
// https://github.com/clams-tech/rune-decoder/blob/57c2e76d1ef9ab7336f565b99de300da1c7b67ce/src/index.ts
|
||||
export const decodeRune = (rune) => {
|
||||
const runeBinary = Base64Binary.decode(rune)
|
||||
|
||||
154
lib/comments.js
154
lib/comments.js
@ -1,87 +1,91 @@
|
||||
import { COMMENTS, HAS_COMMENTS } from '../fragments/comments'
|
||||
import { COMMENT_WITH_NEW_RECURSIVE, COMMENT_WITH_NEW_LIMITED, COMMENT_WITH_NEW_MINIMAL } from '../fragments/comments'
|
||||
import { ITEM_FULL } from '../fragments/items'
|
||||
|
||||
// adds a comment to the cache, under its parent item
|
||||
function cacheComment (cache, newComment, { live = false }) {
|
||||
return cache.modify({
|
||||
id: `Item:${newComment.parentId}`,
|
||||
fields: {
|
||||
comments: (existingComments = {}, { readField }) => {
|
||||
// if the comment already exists, return
|
||||
if (existingComments?.comments?.some(c => readField('id', c) === newComment.id)) return existingComments
|
||||
|
||||
// we need to make sure we're writing a fragment that matches the comments query (comments and count fields)
|
||||
const newCommentRef = cache.writeFragment({
|
||||
data: {
|
||||
comments: {
|
||||
comments: []
|
||||
},
|
||||
ncomments: 0,
|
||||
nDirectComments: 0,
|
||||
...newComment,
|
||||
live
|
||||
},
|
||||
fragment: COMMENTS,
|
||||
fragmentName: 'CommentsRecursive'
|
||||
})
|
||||
|
||||
return {
|
||||
cursor: existingComments.cursor,
|
||||
comments: [newCommentRef, ...(existingComments?.comments || [])]
|
||||
}
|
||||
}
|
||||
},
|
||||
optimistic: true
|
||||
})
|
||||
}
|
||||
|
||||
// handles cache injection and side-effects for both live and non-live comments
|
||||
export function injectComment (cache, newComment, { live = false, rootId } = {}) {
|
||||
// if live and a reply (not top level), check if the parent has comments
|
||||
const hasComments = live && !(Number(rootId) === Number(newComment.parentId))
|
||||
? !!(cache.readFragment({
|
||||
id: `Item:${newComment.parentId}`,
|
||||
fragment: HAS_COMMENTS
|
||||
}))
|
||||
// if not live, we can assume the parent has the comments field since user replied to it
|
||||
: true
|
||||
|
||||
const updated = hasComments && cacheComment(cache, newComment, { live })
|
||||
|
||||
// run side effects if injection succeeded or if injecting live comment into SSR item without comments field
|
||||
if (updated || (live && !hasComments)) {
|
||||
// update all ancestors comment count, excluding the comment itself
|
||||
const ancestors = newComment.path.split('.').slice(0, -1)
|
||||
updateAncestorsCommentCount(cache, ancestors)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// updates the ncomments and nDirectComments fields of all ancestors of an item/comment in the cache
|
||||
function updateAncestorsCommentCount (cache, ancestors, { ncomments = 1, nDirectComments = 1 } = {}) {
|
||||
// update nDirectComments of immediate parent
|
||||
cache.modify({
|
||||
id: `Item:${ancestors[ancestors.length - 1]}`,
|
||||
fields: {
|
||||
nDirectComments (existingNDirectComments = 0) {
|
||||
return existingNDirectComments + nDirectComments
|
||||
}
|
||||
},
|
||||
optimistic: true
|
||||
})
|
||||
|
||||
// update ncomments of all ancestors
|
||||
// updates the ncomments field of all ancestors of an item/comment in the cache
|
||||
export function updateAncestorsCommentCount (cache, ancestors, increment) {
|
||||
// update all ancestors
|
||||
ancestors.forEach(id => {
|
||||
cache.modify({
|
||||
id: `Item:${id}`,
|
||||
fields: {
|
||||
ncomments (existingNComments = 0) {
|
||||
return existingNComments + ncomments
|
||||
return existingNComments + increment
|
||||
}
|
||||
},
|
||||
optimistic: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// updates the item query in the cache
|
||||
// this is used by live comments to update a top level item's comments field
|
||||
export function updateItemQuery (cache, id, sort, fn) {
|
||||
cache.updateQuery({
|
||||
query: ITEM_FULL,
|
||||
// updateQuery needs the correct variables to update the correct item
|
||||
// the Item query might have the router.query.sort in the variables, so we need to pass it in if it exists
|
||||
variables: sort ? { id, sort } : { id }
|
||||
}, (data) => {
|
||||
if (!data) return data
|
||||
return { item: fn(data.item) }
|
||||
})
|
||||
}
|
||||
|
||||
// updates a comment fragment in the cache, with fallbacks for comments lacking CommentsRecursive or Comments altogether
|
||||
export function updateCommentFragment (cache, id, fn) {
|
||||
let result = cache.updateFragment({
|
||||
id: `Item:${id}`,
|
||||
fragment: COMMENT_WITH_NEW_RECURSIVE,
|
||||
fragmentName: 'CommentWithNewRecursive'
|
||||
}, (data) => {
|
||||
if (!data) return data
|
||||
return fn(data)
|
||||
})
|
||||
|
||||
// sometimes comments can start to reach their depth limit, and lack adherence to the CommentsRecursive fragment
|
||||
// for this reason, we update the fragment with a limited version that only includes the CommentFields fragment
|
||||
if (!result) {
|
||||
result = cache.updateFragment({
|
||||
id: `Item:${id}`,
|
||||
fragment: COMMENT_WITH_NEW_LIMITED,
|
||||
fragmentName: 'CommentWithNewLimited'
|
||||
}, (data) => {
|
||||
if (!data) return data
|
||||
return fn(data)
|
||||
})
|
||||
}
|
||||
|
||||
// at the deepest level, the comment can't have any children, here we update only the newComments field.
|
||||
if (!result) {
|
||||
result = cache.updateFragment({
|
||||
id: `Item:${id}`,
|
||||
fragment: COMMENT_WITH_NEW_MINIMAL,
|
||||
fragmentName: 'CommentWithNewMinimal'
|
||||
}, (data) => {
|
||||
if (!data) return data
|
||||
return fn(data)
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function calculateDepth (path, rootId, parentId) {
|
||||
// calculate depth by counting path segments from root to parent
|
||||
const pathSegments = path.split('.')
|
||||
const rootIndex = pathSegments.indexOf(rootId.toString())
|
||||
const parentIndex = pathSegments.indexOf(parentId.toString())
|
||||
|
||||
// depth is the distance from root to parent in the path
|
||||
const depth = parentIndex - rootIndex
|
||||
|
||||
return depth
|
||||
}
|
||||
|
||||
// finds the most recent createdAt timestamp from an array of comments
|
||||
export function getLatestCommentCreatedAt (comments, latest) {
|
||||
return comments.reduce(
|
||||
(max, { createdAt }) => (createdAt > max ? createdAt : max),
|
||||
latest
|
||||
)
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ export const BOUNTY_MAX = 10000000
|
||||
export const POST_TYPES = ['LINK', 'DISCUSSION', 'BOUNTY', 'POLL']
|
||||
export const TERRITORY_BILLING_TYPES = ['MONTHLY', 'YEARLY', 'ONCE']
|
||||
export const TERRITORY_GRACE_DAYS = 5
|
||||
export const COMMENT_DEPTH_LIMIT = 8
|
||||
export const COMMENT_DEPTH_LIMIT = 6
|
||||
export const COMMENTS_LIMIT = 50
|
||||
export const FULL_COMMENTS_THRESHOLD = 200
|
||||
export const COMMENTS_OF_COMMENT_LIMIT = 2
|
||||
@ -59,14 +59,13 @@ export const INV_PENDING_LIMIT = 100
|
||||
export const USER_ID = {
|
||||
k00b: 616,
|
||||
ek: 6030,
|
||||
sox: 26458,
|
||||
sn: 4502,
|
||||
anon: 27,
|
||||
ad: 9,
|
||||
delete: 106,
|
||||
saloon: 17226
|
||||
}
|
||||
export const SN_ADMIN_IDS = [USER_ID.k00b, USER_ID.ek, USER_ID.sox, USER_ID.sn]
|
||||
export const SN_ADMIN_IDS = [USER_ID.k00b, USER_ID.ek, USER_ID.sn]
|
||||
export const SN_NO_REWARDS_IDS = [USER_ID.anon, USER_ID.sn, USER_ID.saloon]
|
||||
export const MAX_POLL_NUM_CHOICES = 10
|
||||
export const MIN_POLL_NUM_CHOICES = 2
|
||||
@ -184,18 +183,16 @@ export const LOST_BLURBS = {
|
||||
|
||||
export const ADMIN_ITEMS = [
|
||||
// FAQ, old privacy policy, changelog, content guidelines, tos, new privacy policy, copyright policy
|
||||
349, 76894, 78763, 81862, 338393, 338369, 338453,
|
||||
// wallet guides: LNbits, phoenixd, Alby Hub, Coinos
|
||||
1212223, 1212375, 1215565, 1230105
|
||||
349, 76894, 78763, 81862, 338393, 338369, 338453
|
||||
]
|
||||
|
||||
export const INVOICE_RETENTION_DAYS = 7
|
||||
export const JIT_INVOICE_TIMEOUT_MS = 180_000
|
||||
|
||||
export const FAST_POLL_INTERVAL_MS = Number(process.env.NEXT_PUBLIC_FAST_POLL_INTERVAL_MS)
|
||||
export const NORMAL_POLL_INTERVAL_MS = Number(process.env.NEXT_PUBLIC_NORMAL_POLL_INTERVAL_MS)
|
||||
export const LONG_POLL_INTERVAL_MS = Number(process.env.NEXT_PUBLIC_LONG_POLL_INTERVAL_MS)
|
||||
export const EXTRA_LONG_POLL_INTERVAL_MS = Number(process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL_MS)
|
||||
export const FAST_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_FAST_POLL_INTERVAL)
|
||||
export const NORMAL_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_NORMAL_POLL_INTERVAL)
|
||||
export const LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_LONG_POLL_INTERVAL)
|
||||
export const EXTRA_LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL)
|
||||
|
||||
export const ZAP_UNDO_DELAY_MS = 5_000
|
||||
|
||||
|
||||
@ -22,14 +22,7 @@ export function commentsViewedAfterComment (rootId, createdAt, ncomments = 1) {
|
||||
}
|
||||
|
||||
export function newComments (item) {
|
||||
if (!item.parentId && item.lastCommentAt) {
|
||||
// if logged, prefer server-tracked view
|
||||
if (item.meCommentsViewedAt) {
|
||||
const viewedAt = new Date(item.meCommentsViewedAt).getTime()
|
||||
return viewedAt < new Date(item.lastCommentAt).getTime()
|
||||
}
|
||||
|
||||
// anon fallback
|
||||
if (!item.parentId) {
|
||||
const viewedAt = commentsViewedAt(item.id)
|
||||
const viewNum = commentsViewedNum(item.id)
|
||||
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import { SKIP, visit } from 'unist-util-visit'
|
||||
import { extractHeadings } from './toc'
|
||||
|
||||
export default function remarkToc () {
|
||||
return function transformer (tree) {
|
||||
const headings = extractHeadings(tree)
|
||||
|
||||
visit(tree, 'paragraph', (node, index, parent) => {
|
||||
if (
|
||||
node.children?.length === 1 &&
|
||||
node.children[0].type === 'text' &&
|
||||
node.children[0].value.trim() === '{:toc}'
|
||||
) {
|
||||
parent.children.splice(index, 1, buildToc(headings))
|
||||
return [SKIP, index]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function buildToc (headings) {
|
||||
const root = { type: 'list', ordered: false, spread: false, children: [] }
|
||||
const stack = [{ depth: 0, node: root }] // holds the current chain of parents
|
||||
|
||||
for (const { heading, slug, depth } of headings) {
|
||||
// walk up the stack to find the parent of the current heading
|
||||
while (stack.length && depth <= stack[stack.length - 1].depth) {
|
||||
stack.pop()
|
||||
}
|
||||
let parent = stack[stack.length - 1].node
|
||||
|
||||
// if the parent is a li, gets its child ul
|
||||
if (parent.type === 'listItem') {
|
||||
let ul = parent.children.find(c => c.type === 'list')
|
||||
if (!ul) {
|
||||
ul = { type: 'list', ordered: false, spread: false, children: [] }
|
||||
parent.children.push(ul)
|
||||
}
|
||||
parent = ul
|
||||
}
|
||||
|
||||
// build the li from the current heading
|
||||
const listItem = {
|
||||
type: 'listItem',
|
||||
spread: false,
|
||||
children: [{
|
||||
type: 'paragraph',
|
||||
children: [{
|
||||
type: 'link',
|
||||
url: `#${slug}`,
|
||||
children: [{ type: 'text', value: heading }]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
parent.children.push(listItem)
|
||||
stack.push({ depth, node: listItem })
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
23
lib/toc.js
23
lib/toc.js
@ -1,23 +0,0 @@
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import { slug } from 'github-slugger'
|
||||
|
||||
export function extractHeadings (markdownOrTree) {
|
||||
const tree = typeof markdownOrTree === 'string'
|
||||
? fromMarkdown(markdownOrTree)
|
||||
: markdownOrTree
|
||||
|
||||
const headings = []
|
||||
|
||||
visit(tree, 'heading', node => {
|
||||
const str = toString(node)
|
||||
headings.push({
|
||||
heading: str,
|
||||
slug: slug(str.replace(/[^\w\-\s]+/gi, '')),
|
||||
depth: node.depth
|
||||
})
|
||||
})
|
||||
|
||||
return headings
|
||||
}
|
||||
15
lib/url.js
15
lib/url.js
@ -78,17 +78,6 @@ export function parseInternalLinks (href) {
|
||||
return {}
|
||||
}
|
||||
|
||||
export function parseYoutubeStart (t) {
|
||||
// https://stackoverflow.com/questions/17379268/youtube-dropped-t-start-time-support-in-direct-url-and-embed-videos
|
||||
// https://developers.google.com/youtube/player_parameters#start
|
||||
if (!t || !t.match(/^([0-9]+[smh])+$/g)) return t
|
||||
let r = 0
|
||||
for (const m of t.matchAll(/([0-9]+)([smh])/g)) {
|
||||
r += parseInt(m[1]) * Math.pow(60, 'smh'.indexOf(m[2]))
|
||||
}
|
||||
return r.toString()
|
||||
}
|
||||
|
||||
export function parseEmbedUrl (href) {
|
||||
if (!href) return null
|
||||
|
||||
@ -143,7 +132,7 @@ export function parseEmbedUrl (href) {
|
||||
id: searchParams.get('v'),
|
||||
meta: {
|
||||
href,
|
||||
start: parseYoutubeStart(searchParams.get('t'))
|
||||
start: searchParams.get('t')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,7 +152,7 @@ export function parseEmbedUrl (href) {
|
||||
id: pathname.slice(1), // remove leading slash
|
||||
meta: {
|
||||
href,
|
||||
start: parseYoutubeStart(searchParams.get('t'))
|
||||
start: searchParams.get('t')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,10 +116,6 @@ async function subHasPostType (name, type, { client, models }) {
|
||||
return !!(sub?.postTypes?.includes(type))
|
||||
}
|
||||
|
||||
export const searchSchema = object({
|
||||
q: string().trim().max(100, 'must be at most 100 characters')
|
||||
})
|
||||
|
||||
export function advPostSchemaMembers ({ me, existingBoost = 0, ...args }) {
|
||||
const boostMin = existingBoost || BOOST_MIN
|
||||
return {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const { withPlausibleProxy } = require('next-plausible')
|
||||
const { InjectManifest } = require('workbox-webpack-plugin')
|
||||
const CopyPlugin = require('copy-webpack-plugin')
|
||||
const { generatePrecacheManifest } = require('./sw/build.js')
|
||||
const webpack = require('webpack')
|
||||
|
||||
let isProd = process.env.NODE_ENV === 'production'
|
||||
@ -215,24 +215,15 @@ module.exports = withPlausibleProxy()({
|
||||
},
|
||||
webpack: (config, { isServer, dev, defaultLoaders }) => {
|
||||
if (isServer) {
|
||||
generatePrecacheManifest()
|
||||
const workboxPlugin = new InjectManifest({
|
||||
include: [/\/(icons|maskable|splash)\//, /\.(webp|ttf|woff|woff2)$/],
|
||||
// ignore the precached manifest which includes the webpack assets
|
||||
// since they are not useful to us
|
||||
exclude: [/.*/],
|
||||
// by default, webpack saves service worker at .next/server/
|
||||
swDest: '../../public/sw.js',
|
||||
swSrc: './sw/index.js',
|
||||
webpackCompilationPlugins: [
|
||||
// we want to precache these static assets so we copy them to include them in the webpack pipeline
|
||||
// so InjectManifest can inject them into the service worker manifest
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: 'public/icons', to: '../icons' },
|
||||
{ from: 'public/maskable', to: '../maskable' },
|
||||
{ from: 'public/splash', to: '../splash' },
|
||||
{ from: 'public/waiting.webp', to: '../waiting.webp' },
|
||||
{ from: 'public/Lightningvolt-xoqm.ttf', to: '../Lightningvolt-xoqm.ttf' },
|
||||
{ from: 'public/Lightningvolt-xoqm.woff', to: '../Lightningvolt-xoqm.woff' },
|
||||
{ from: 'public/Lightningvolt-xoqm.woff2', to: '../Lightningvolt-xoqm.woff2' }
|
||||
]
|
||||
}),
|
||||
// this is need to allow the service worker to access these environment variables
|
||||
// from lib/constants.js
|
||||
new webpack.DefinePlugin({
|
||||
@ -241,10 +232,10 @@ module.exports = withPlausibleProxy()({
|
||||
'process.env.NEXT_PUBLIC_MEDIA_URL': JSON.stringify(process.env.NEXT_PUBLIC_MEDIA_URL),
|
||||
'process.env.NEXT_PUBLIC_MEDIA_DOMAIN': JSON.stringify(process.env.NEXT_PUBLIC_MEDIA_DOMAIN),
|
||||
'process.env.NEXT_PUBLIC_URL': JSON.stringify(process.env.NEXT_PUBLIC_URL),
|
||||
'process.env.NEXT_PUBLIC_FAST_POLL_INTERVAL_MS': JSON.stringify(process.env.NEXT_PUBLIC_FAST_POLL_INTERVAL_MS),
|
||||
'process.env.NEXT_PUBLIC_NORMAL_POLL_INTERVAL_MS': JSON.stringify(process.env.NEXT_PUBLIC_NORMAL_POLL_INTERVAL_MS),
|
||||
'process.env.NEXT_PUBLIC_LONG_POLL_INTERVAL_MS': JSON.stringify(process.env.NEXT_PUBLIC_LONG_POLL_INTERVAL_MS),
|
||||
'process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL_MS': JSON.stringify(process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL_MS),
|
||||
'process.env.NEXT_PUBLIC_FAST_POLL_INTERVAL': JSON.stringify(process.env.NEXT_PUBLIC_FAST_POLL_INTERVAL),
|
||||
'process.env.NEXT_PUBLIC_NORMAL_POLL_INTERVAL': JSON.stringify(process.env.NEXT_PUBLIC_NORMAL_POLL_INTERVAL),
|
||||
'process.env.NEXT_PUBLIC_LONG_POLL_INTERVAL': JSON.stringify(process.env.NEXT_PUBLIC_LONG_POLL_INTERVAL),
|
||||
'process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL': JSON.stringify(process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL),
|
||||
'process.env.SANCTIONED_COUNTRY_CODES': JSON.stringify(process.env.SANCTIONED_COUNTRY_CODES),
|
||||
'process.env.NEXT_IS_EXPORT_WORKER': 'true'
|
||||
})
|
||||
|
||||
347
package-lock.json
generated
347
package-lock.json
generated
@ -20,7 +20,6 @@
|
||||
"@nostr-dev-kit/ndk-wallet": "^0.5.0",
|
||||
"@opensearch-project/opensearch": "^2.12.0",
|
||||
"@prisma/client": "^5.20.0",
|
||||
"@shocknet/clink-sdk": "^1.4.0",
|
||||
"@slack/web-api": "^7.6.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@yudiel/react-qr-scanner": "^2.0.8",
|
||||
@ -36,7 +35,6 @@
|
||||
"classnames": "^2.5.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"cookie": "^1.0.1",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"csv-parser": "^3.0.0",
|
||||
"domino": "^2.1.6",
|
||||
@ -3237,31 +3235,11 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
@ -3278,6 +3256,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -3289,6 +3268,7 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -3299,12 +3279,14 @@
|
||||
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
@ -3321,6 +3303,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
@ -3335,6 +3318,7 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
@ -5284,137 +5268,6 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@shocknet/clink-sdk/-/clink-sdk-1.4.0.tgz",
|
||||
"integrity": "sha512-J0PWE8CVRJrFF1Zi/UhChhvOrlmDj7LRJTpR6rbHlFPmjC5TGIW6891tVWWv+JmUR0jzez9QHFrHnc8DgIJYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@scure/base": "^1.2.5",
|
||||
"nostr-tools": "^2.13.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk/node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk/node_modules/@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
|
||||
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk/node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk/node_modules/jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk/node_modules/lru-cache": {
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
|
||||
"integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk/node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk/node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@shocknet/clink-sdk/node_modules/rimraf": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
|
||||
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^11.0.0",
|
||||
"package-json-from-dist": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
@ -6655,23 +6508,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
@ -8212,60 +8048,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"node_modules/copy-webpack-plugin": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz",
|
||||
"integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob-parent": "^6.0.1",
|
||||
"normalize-path": "^3.0.0",
|
||||
"schema-utils": "^4.2.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"tinyglobby": "^0.2.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin/node_modules/schema-utils": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.9.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"ajv-keywords": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.38.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz",
|
||||
@ -8454,10 +8236,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@ -9139,7 +8921,8 @@
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
@ -10451,23 +10234,6 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@ -10611,12 +10377,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"license": "ISC",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"cross-spawn": "^7.0.0",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -10630,6 +10396,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@ -10953,6 +10720,7 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
@ -12226,7 +11994,8 @@
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/isomorphic-ws": {
|
||||
"version": "5.0.0",
|
||||
@ -16017,6 +15786,7 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@ -16285,6 +16055,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -16299,18 +16070,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-tools": {
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.16.2.tgz",
|
||||
"integrity": "sha512-ZxH9EbSt5ypURZj2TGNJxZd0Omb5ag5KZSu8IyJMCdLyg2KKz+2GA0sP/cSawCQEkyviIN4eRT4G2gB/t9lMRw==",
|
||||
"license": "Unlicense",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.8.0.tgz",
|
||||
"integrity": "sha512-aumZBa9Ok/cAJLovSBCIA/DkJjLjF/Hs5DpQGEjmyfaUkGBqd5jZjzalcVMyy/9HkkRZfJmbTPtqHTKFNvBSHQ==",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.5.1",
|
||||
"@noble/curves": "1.2.0",
|
||||
"@noble/hashes": "1.3.1",
|
||||
"@scure/base": "1.1.1",
|
||||
"@scure/bip32": "1.3.1",
|
||||
"@scure/bip39": "1.2.1",
|
||||
"nostr-wasm": "0.1.0"
|
||||
"@scure/bip39": "1.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"nostr-wasm": "v0.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
@ -16346,7 +16118,8 @@
|
||||
"node_modules/nostr-wasm": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="
|
||||
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
@ -16726,12 +16499,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/packet-reader": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
|
||||
@ -16827,6 +16594,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@ -18844,6 +18612,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
@ -18855,6 +18624,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@ -19511,6 +19281,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@ -19630,6 +19401,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@ -19905,34 +19677,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.51",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.51.tgz",
|
||||
@ -20461,9 +20205,9 @@
|
||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -21326,6 +21070,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
@ -21678,6 +21423,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
@ -21694,6 +21440,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
@ -21708,6 +21455,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
@ -21718,7 +21466,8 @@
|
||||
"node_modules/wrap-ansi-cjs/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
|
||||
@ -25,7 +25,6 @@
|
||||
"@nostr-dev-kit/ndk-wallet": "^0.5.0",
|
||||
"@opensearch-project/opensearch": "^2.12.0",
|
||||
"@prisma/client": "^5.20.0",
|
||||
"@shocknet/clink-sdk": "^1.4.0",
|
||||
"@slack/web-api": "^7.6.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@yudiel/react-qr-scanner": "^2.0.8",
|
||||
@ -41,7 +40,6 @@
|
||||
"classnames": "^2.5.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"cookie": "^1.0.1",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"csv-parser": "^3.0.0",
|
||||
"domino": "^2.1.6",
|
||||
|
||||
@ -20,7 +20,6 @@ import { ChainFeeProvider } from '@/components/chain-fee.js'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
||||
import WalletsProvider from '@/wallets/client/context'
|
||||
import FaviconProvider from '@/components/favicon'
|
||||
|
||||
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
||||
|
||||
@ -60,6 +59,14 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
||||
router.events.on('routeChangeComplete', nprogressDone)
|
||||
router.events.on('routeChangeError', nprogressDone)
|
||||
|
||||
const handleServiceWorkerMessage = (event) => {
|
||||
if (event.data?.type === 'navigate') {
|
||||
router.push(event.data.url)
|
||||
}
|
||||
}
|
||||
|
||||
navigator.serviceWorker?.addEventListener('message', handleServiceWorkerMessage)
|
||||
|
||||
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
|
||||
@ -82,6 +89,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
||||
router.events.off('routeChangeStart', nprogressStart)
|
||||
router.events.off('routeChangeComplete', nprogressDone)
|
||||
router.events.off('routeChangeError', nprogressDone)
|
||||
navigator.serviceWorker?.removeEventListener('message', handleServiceWorkerMessage)
|
||||
}
|
||||
}, [router.asPath, props?.apollo, shouldShowProgressBar])
|
||||
|
||||
@ -113,26 +121,24 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
||||
<MeProvider me={me}>
|
||||
<WalletsProvider>
|
||||
<HasNewNotesProvider>
|
||||
<FaviconProvider>
|
||||
<ServiceWorkerProvider>
|
||||
<PriceProvider price={price}>
|
||||
<AnimationProvider>
|
||||
<ToastProvider>
|
||||
<ShowModalProvider>
|
||||
<BlockHeightProvider blockHeight={blockHeight}>
|
||||
<ChainFeeProvider chainFee={chainFee}>
|
||||
<ErrorBoundary>
|
||||
<Component ssrData={ssrData} {...otherProps} />
|
||||
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
|
||||
</ErrorBoundary>
|
||||
</ChainFeeProvider>
|
||||
</BlockHeightProvider>
|
||||
</ShowModalProvider>
|
||||
</ToastProvider>
|
||||
</AnimationProvider>
|
||||
</PriceProvider>
|
||||
</ServiceWorkerProvider>
|
||||
</FaviconProvider>
|
||||
<ServiceWorkerProvider>
|
||||
<PriceProvider price={price}>
|
||||
<AnimationProvider>
|
||||
<ToastProvider>
|
||||
<ShowModalProvider>
|
||||
<BlockHeightProvider blockHeight={blockHeight}>
|
||||
<ChainFeeProvider chainFee={chainFee}>
|
||||
<ErrorBoundary>
|
||||
<Component ssrData={ssrData} {...otherProps} />
|
||||
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
|
||||
</ErrorBoundary>
|
||||
</ChainFeeProvider>
|
||||
</BlockHeightProvider>
|
||||
</ShowModalProvider>
|
||||
</ToastProvider>
|
||||
</AnimationProvider>
|
||||
</PriceProvider>
|
||||
</ServiceWorkerProvider>
|
||||
</HasNewNotesProvider>
|
||||
</WalletsProvider>
|
||||
</MeProvider>
|
||||
|
||||
@ -327,43 +327,38 @@ export const getAuthOptions = (req, res) => ({
|
||||
return user
|
||||
},
|
||||
useVerificationToken: async ({ identifier, token }) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const [verificationRequest] = await tx.$queryRaw`
|
||||
UPDATE verification_requests
|
||||
SET attempts = attempts + 1
|
||||
FROM (
|
||||
SELECT id FROM verification_requests
|
||||
WHERE identifier = ${identifier}
|
||||
AND created_at > NOW() - INTERVAL '5 minutes'
|
||||
-- we need to find the most recent verification request for this email/identifier
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
) for_update
|
||||
WHERE verification_requests.id = for_update.id
|
||||
RETURNING *
|
||||
`
|
||||
if (!verificationRequest) throw new Error('No verification request found')
|
||||
|
||||
if (verificationRequest.token === token) {
|
||||
// correct token was entered, delete the verification request because we no longer need it
|
||||
await tx.verificationToken.delete({
|
||||
where: { id: verificationRequest.id }
|
||||
})
|
||||
return verificationRequest
|
||||
// we need to find the most recent verification request for this email/identifier
|
||||
const verificationRequest = await prisma.verificationToken.findFirst({
|
||||
where: {
|
||||
identifier,
|
||||
attempts: {
|
||||
lt: 2 // count starts at 0
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
|
||||
if (verificationRequest.attempts >= 3) {
|
||||
// too many attempts, delete the verification request and redirect to error page by throwing an error
|
||||
await tx.verificationToken.delete({
|
||||
where: { id: verificationRequest.id }
|
||||
})
|
||||
throw new Error('too many attempts')
|
||||
}
|
||||
|
||||
// wrong code but can try again
|
||||
return null
|
||||
})
|
||||
|
||||
if (!verificationRequest) throw new Error('No verification request found')
|
||||
|
||||
if (verificationRequest.token === token) { // if correct delete the token and continue
|
||||
await prisma.verificationToken.delete({
|
||||
where: { id: verificationRequest.id }
|
||||
})
|
||||
return verificationRequest
|
||||
}
|
||||
|
||||
await prisma.verificationToken.update({
|
||||
where: { id: verificationRequest.id },
|
||||
data: { attempts: { increment: 1 } }
|
||||
})
|
||||
|
||||
await prisma.verificationToken.deleteMany({
|
||||
where: { id: verificationRequest.id, attempts: { gte: 2 } }
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
},
|
||||
session: {
|
||||
@ -413,9 +408,7 @@ function generateRandomString (length = 6, charset = BECH32_CHARSET) {
|
||||
const bytes = randomBytes(length)
|
||||
let result = ''
|
||||
|
||||
// Even though we're creating biased numbers by mapping each byte to a bech32 character,
|
||||
// this is still secure because it provides 30 bits of security (32^6 = 2^30)
|
||||
// and we are limiting the number of attempts.
|
||||
// Map each byte to a character in the charset
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[bytes[i] % charset.length]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user