From 511b0eea409e1fa6da801716069c9178b55ded70 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 7 Jan 2025 15:18:35 +0100 Subject: [PATCH 1/5] fix tooltip blinking at the edges (#1813) --- components/badge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/badge.js b/components/badge.js index 5853b6221..e1b6a162d 100644 --- a/components/badge.js +++ b/components/badge.js @@ -75,7 +75,7 @@ export function BadgeTooltip ({ children, overlayText, placement }) { + {overlayText} } From 44d3748d5ffe8e96c15c1cea2e7cf1e51e5ceab8 Mon Sep 17 00:00:00 2001 From: soxa <6390896+Soxasora@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:40:25 +0100 Subject: [PATCH 2/5] fix: iOS PWA push notifications (#1794) * fix: iOS pwa push notifications * fix lint * align onNotificationClick promises to event.waitUntil * align CLEAR_NOTIFICATIONS promises to event.waitUntil * include notifications url for merged payloads * hotfix: track amount via notification.length * better comments --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> --- lib/badge.js | 18 +++-- sw/eventListener.js | 186 ++++++++++++++++++-------------------------- 2 files changed, 85 insertions(+), 119 deletions(-) diff --git a/lib/badge.js b/lib/badge.js index 78ad505fb..1b444c2dc 100644 --- a/lib/badge.js +++ b/lib/badge.js @@ -4,7 +4,8 @@ export const clearNotifications = () => navigator.serviceWorker?.controller?.pos const badgingApiSupported = (sw = window) => 'setAppBadge' in sw.navigator -const permissionGranted = async (sw = window) => { +// we don't need this, we can use the badging API +/* const permissionGranted = async (sw = window) => { const name = 'notifications' let permission try { @@ -13,21 +14,22 @@ const permissionGranted = async (sw = window) => { console.error('Failed to check permissions', err) } return permission?.state === 'granted' || sw.Notification?.permission === 'granted' -} +} */ -export const setAppBadge = async (sw = window, count) => { - if (!badgingApiSupported(sw) || !(await permissionGranted(sw))) return +// Apple requirement: onPush doesn't accept async functions +export const setAppBadge = (sw = window, count) => { + if (!badgingApiSupported(sw)) return try { - await sw.navigator.setAppBadge(count) + return sw.navigator.setAppBadge(count) // Return a Promise to be handled } catch (err) { console.error('Failed to set app badge', err) } } -export const clearAppBadge = async (sw = window) => { - if (!badgingApiSupported(sw) || !(await permissionGranted(sw))) return +export const clearAppBadge = (sw = window) => { + if (!badgingApiSupported(sw)) return try { - await sw.navigator.clearAppBadge() + return sw.navigator.clearAppBadge() // Return a Promise to be handled } catch (err) { console.error('Failed to clear app badge', err) } diff --git a/sw/eventListener.js b/sw/eventListener.js index d2d0bbcc8..fab3e910a 100644 --- a/sw/eventListener.js +++ b/sw/eventListener.js @@ -2,8 +2,9 @@ import ServiceWorkerStorage from 'serviceworker-storage' import { numWithUnits } from '@/lib/format' import { CLEAR_NOTIFICATIONS, clearAppBadge, setAppBadge } from '@/lib/badge' import { ACTION_PORT, DELETE_SUBSCRIPTION, MESSAGE_PORT, STORE_OS, STORE_SUBSCRIPTION, SYNC_SUBSCRIPTION } from '@/components/serviceworker' +// import { getLogger } from '@/lib/logger' -// we store existing push subscriptions to keep them in sync with server +// we store existing push subscriptions and OS to keep them in sync with server const storage = new ServiceWorkerStorage('sw:storage', 1) // for communication between app and service worker @@ -23,99 +24,69 @@ async function getOS () { // current push notification count for badge purposes let activeCount = 0 +// message event listener for communication between app and service worker const log = (message, level = 'info', context) => { messageChannelPort?.postMessage({ level, message, context }) } export function onPush (sw) { - return async (event) => { - const payload = event.data?.json() - if (!payload) return + return (event) => { + // in case of push notifications, make sure that the logger has an HTTPS endpoint + // const logger = getLogger('sw:push', ['onPush']) + let payload = event.data?.json() + if (!payload) return // ignore push events without payload, like isTrusted events const { tag } = payload.options - event.waitUntil((async () => { - const iOS = await getOS() === 'iOS' - // generate random ID for every incoming push for better tracing in logs - const nid = crypto.randomUUID() - log(`[sw:push] ${nid} - received notification with tag ${tag}`) - - // due to missing proper tag support in Safari on iOS, we can't rely on the tag built-in filter. - // we therefore fetch all notifications with the same tag and manually filter them, too. - // see https://bugs.webkit.org/show_bug.cgi?id=258922 - const notifications = await sw.registration.getNotifications({ tag }) - log(`[sw:push] ${nid} - found ${notifications.length} ${tag} notifications`) - log(`[sw:push] ${nid} - built-in tag filter: ${JSON.stringify(notifications.map(({ tag }) => tag))}`) + const nid = crypto.randomUUID() // notification id for tracking - // we're not sure if the built-in tag filter actually filters by tag on iOS - // or if it just returns all currently displayed notifications (?) - const filtered = notifications.filter(({ tag: nTag }) => nTag === tag) - log(`[sw:push] ${nid} - found ${filtered.length} ${tag} notifications after manual tag filter`) - log(`[sw:push] ${nid} - manual tag filter: ${JSON.stringify(filtered.map(({ tag }) => tag))}`) + // iOS requirement: group all promises + const promises = [] - if (immediatelyShowNotification(tag)) { - // we can't rely on the tag property to replace notifications on Safari on iOS. - // we therefore close them manually and then we display the notification. - log(`[sw:push] ${nid} - ${tag} notifications replace previous notifications`) - setAppBadge(sw, ++activeCount) - // due to missing proper tag support in Safari on iOS, we can't rely on the tag property to replace notifications. - // see https://bugs.webkit.org/show_bug.cgi?id=258922 for more information - // we therefore fetch all notifications with the same tag (+ manual filter), - // close them and then we display the notification. - const notifications = await sw.registration.getNotifications({ tag }) - // we only close notifications manually on iOS because we don't want to degrade android UX just because iOS is behind in their support. - if (iOS) { - log(`[sw:push] ${nid} - closing existing notifications`) - notifications.filter(({ tag: nTag }) => nTag === tag).forEach(n => n.close()) + // On immediate notifications we update the counter + if (immediatelyShowNotification(tag)) { + // logger.info(`[${nid}] showing immediate notification with title: ${payload.title}`) + promises.push(setAppBadge(sw, ++activeCount)) + } else { + // logger.info(`[${nid}] checking for existing notification with tag ${tag}`) + // Check if there are already notifications with the same tag and merge them + promises.push(sw.registration.getNotifications({ tag }).then((notifications) => { + // logger.info(`[${nid}] found ${notifications.length} notifications with tag ${tag}`) + if (notifications.length) { + // logger.info(`[${nid}] found ${notifications.length} notifications with tag ${tag}`) + payload = mergeNotification(event, sw, payload, notifications, tag, nid) } - log(`[sw:push] ${nid} - show notification with title "${payload.title}"`) - return await sw.registration.showNotification(payload.title, payload.options) - } - - // according to the spec, there should only be zero or one notification since we used a tag filter - // handle zero case here - if (notifications.length === 0) { - // incoming notification is first notification with this tag - log(`[sw:push] ${nid} - no existing ${tag} notifications found`) - setAppBadge(sw, ++activeCount) - log(`[sw:push] ${nid} - show notification with title "${payload.title}"`) - return await sw.registration.showNotification(payload.title, payload.options) - } - - // handle unexpected case here - if (notifications.length > 1) { - log(`[sw:push] ${nid} - more than one notification with tag ${tag} found`, 'error') - // due to missing proper tag support in Safari on iOS, - // we only acknowledge this error in our logs and don't bail here anymore - // see https://bugs.webkit.org/show_bug.cgi?id=258922 for more information - log(`[sw:push] ${nid} - skip bail -- merging notifications with tag ${tag} manually`) - // return null - } + })) + } - return await mergeAndShowNotification(sw, payload, notifications, tag, nid, iOS) - })()) + // iOS requirement: wait for all promises to resolve before showing the notification + event.waitUntil(Promise.all(promises).then(() => { + sw.registration.showNotification(payload.title, payload.options) + })) } } -// if there is no tag or it's a TIP, FORWARDEDTIP or EARN notification -// we don't need to merge notifications and thus the notification should be immediately shown using `showNotification` +// if there is no tag or the tag is one of the following +// we show the notification immediately const immediatelyShowNotification = (tag) => !tag || ['TIP', 'FORWARDEDTIP', 'EARN', 'STREAK', 'TERRITORY_TRANSFER'].includes(tag.split('-')[0]) -const mergeAndShowNotification = async (sw, payload, currentNotifications, tag, nid, iOS) => { +// merge notifications with the same tag +const mergeNotification = (event, sw, payload, currentNotifications, tag, nid) => { + // const logger = getLogger('sw:push:mergeNotification', ['mergeNotification']) + // sanity check const otherTagNotifications = currentNotifications.filter(({ tag: nTag }) => nTag !== tag) if (otherTagNotifications.length > 0) { // we can't recover from this here. bail. - const message = `[sw:push] ${nid} - bailing -- more than one notification with tag ${tag} found after manual filter` - log(message, 'error') + // logger.error(`${nid} - bailing -- more than one notification with tag ${tag} found after manual filter`) return } const { data: incomingData } = payload.options - log(`[sw:push] ${nid} - incoming payload.options.data: ${JSON.stringify(incomingData)}`) + // logger.info(`[sw:push] ${nid} - incoming payload.options.data: ${JSON.stringify(incomingData)}`) // we can ignore everything after the first dash in the tag for our control flow const compareTag = tag.split('-')[0] - log(`[sw:push] ${nid} - using ${compareTag} for control flow`) + // logger.info(`[sw:push] ${nid} - using ${compareTag} for control flow`) // merge notifications into single notification payload // --- @@ -124,22 +95,20 @@ const mergeAndShowNotification = async (sw, payload, currentNotifications, tag, // tags that need to know the sum of sats of notifications with same tag for merging const SUM_SATS_TAGS = ['DEPOSIT', 'WITHDRAWAL'] // this should reflect the amount of notifications that were already merged before - let initialAmount = currentNotifications[0]?.data?.amount || 1 - if (iOS) initialAmount = 1 - log(`[sw:push] ${nid} - initial amount: ${initialAmount}`) - const mergedPayload = currentNotifications.reduce((acc, { data }) => { - let newAmount, newSats - if (AMOUNT_TAGS.includes(compareTag)) { - newAmount = acc.amount + 1 - } - if (SUM_SATS_TAGS.includes(compareTag)) { - newSats = acc.sats + data.sats - } - const newPayload = { ...data, amount: newAmount, sats: newSats } - return newPayload - }, { ...incomingData, amount: initialAmount }) + const initialAmount = currentNotifications.length || 1 + const initialSats = currentNotifications[0]?.data?.sats || 0 + // logger.info(`[sw:push] ${nid} - initial amount: ${initialAmount}`) + // logger.info(`[sw:push] ${nid} - initial sats: ${initialSats}`) - log(`[sw:push] ${nid} - merged payload: ${JSON.stringify(mergedPayload)}`) + // currentNotifications.reduce causes iOS to sum n notifications + initialAmount which is already n notifications + const mergedPayload = { + ...incomingData, + url: '/notifications', // when merged we should always go to the notifications page + amount: initialAmount + 1, + sats: initialSats + incomingData.sats + } + + // logger.info(`[sw:push] ${nid} - merged payload: ${JSON.stringify(mergedPayload)}`) // calculate title from merged payload const { amount, followeeName, subName, subType, sats } = mergedPayload @@ -167,32 +136,30 @@ const mergeAndShowNotification = async (sw, payload, currentNotifications, tag, title = `${numWithUnits(sats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} withdrawn from your account` } } - log(`[sw:push] ${nid} - calculated title: ${title}`) - - // close all current notifications before showing new one to "merge" notifications - // we only do this on iOS because we don't want to degrade android UX just because iOS is behind in their support. - if (iOS) { - log(`[sw:push] ${nid} - closing existing notifications`) - currentNotifications.forEach(n => n.close()) - } + // logger.info(`[sw:push] ${nid} - calculated title: ${title}`) - const options = { icon: payload.options?.icon, tag, data: { url: '/notifications', ...mergedPayload } } - log(`[sw:push] ${nid} - show notification with title "${title}"`) - return await sw.registration.showNotification(title, options) + const options = { icon: payload.options?.icon, tag, data: { ...mergedPayload } } + // logger.info(`[sw:push] ${nid} - show notification with title "${title}"`) + return { title, options } // send the new, merged, payload } +// iOS-specific bug, notificationclick event only works when the app is closed export function onNotificationClick (sw) { return (event) => { + const promises = [] + // const logger = getLogger('sw:onNotificationClick', ['onNotificationClick']) const url = event.notification.data?.url + // logger.info(`[sw:onNotificationClick] clicked notification with url ${url}`) if (url) { - event.waitUntil(sw.clients.openWindow(url)) + promises.push(sw.clients.openWindow(url)) } activeCount = Math.max(0, activeCount - 1) if (activeCount === 0) { - clearAppBadge(sw) + promises.push(clearAppBadge(sw)) } else { - setAppBadge(sw, activeCount) + promises.push(setAppBadge(sw, activeCount)) } + event.waitUntil(Promise.all(promises)) event.notification.close() } } @@ -202,10 +169,11 @@ export function onPushSubscriptionChange (sw) { // `isSync` is passed if function was called because of 'SYNC_SUBSCRIPTION' event // this makes sure we can differentiate between 'pushsubscriptionchange' events and our custom 'SYNC_SUBSCRIPTION' event return async (event, isSync) => { + // const logger = getLogger('sw:onPushSubscriptionChange', ['onPushSubscriptionChange']) let { oldSubscription, newSubscription } = event // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event // fallbacks since browser may not set oldSubscription and newSubscription - log('[sw:handlePushSubscriptionChange] invoked') + // logger.info('[sw:handlePushSubscriptionChange] invoked') oldSubscription ??= await storage.getItem('subscription') newSubscription ??= await sw.registration.pushManager.getSubscription() if (!newSubscription) { @@ -214,17 +182,17 @@ export function onPushSubscriptionChange (sw) { // see https://github.com/stackernews/stacker.news/issues/411#issuecomment-1790675861 // NOTE: this is only run on IndexedDB subscriptions stored under service worker version 2 since this is not backwards compatible // see discussion in https://github.com/stackernews/stacker.news/pull/597 - log('[sw:handlePushSubscriptionChange] service worker lost subscription') + // logger.info('[sw:handlePushSubscriptionChange] service worker lost subscription') actionChannelPort?.postMessage({ action: 'RESUBSCRIBE' }) return } // no subscription exists at the moment - log('[sw:handlePushSubscriptionChange] no existing subscription found') + // logger.info('[sw:handlePushSubscriptionChange] no existing subscription found') return } if (oldSubscription?.endpoint === newSubscription.endpoint) { // subscription did not change. no need to sync with server - log('[sw:handlePushSubscriptionChange] old subscription matches existing subscription') + // logger.info('[sw:handlePushSubscriptionChange] old subscription matches existing subscription') return } // convert keys from ArrayBuffer to string @@ -249,7 +217,7 @@ export function onPushSubscriptionChange (sw) { }, body }) - log('[sw:handlePushSubscriptionChange] synced push subscription with server', 'info', { endpoint: variables.endpoint, oldEndpoint: variables.oldEndpoint }) + // logger.info('[sw:handlePushSubscriptionChange] synced push subscription with server', 'info', { endpoint: variables.endpoint, oldEndpoint: variables.oldEndpoint }) await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription))) } } @@ -281,17 +249,13 @@ export function onMessage (sw) { return event.waitUntil(storage.removeItem('subscription')) } if (event.data.action === CLEAR_NOTIFICATIONS) { - return event.waitUntil((async () => { - let notifications = [] - try { - notifications = await sw.registration.getNotifications() - } catch (err) { - console.error('failed to get notifications') - } + const promises = [] + promises.push(sw.registration.getNotifications().then((notifications) => { notifications.forEach(notification => notification.close()) - activeCount = 0 - return await clearAppBadge(sw) - })()) + })) + activeCount = 0 + promises.push(clearAppBadge(sw)) + event.waitUntil(Promise.all(promises)) } } } From 466620ff05d0858fa1112209a70e47698c2277f1 Mon Sep 17 00:00:00 2001 From: soxa <6390896+Soxasora@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:51:07 +0100 Subject: [PATCH 3/5] fix: zapping too fast causes duplicate notifications (#1812) * fix: zapping too fast causes duplicate notifications * add comment --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> --- api/paidAction/zap.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/paidAction/zap.js b/api/paidAction/zap.js index 8705d7aa7..9e2b4e22e 100644 --- a/api/paidAction/zap.js +++ b/api/paidAction/zap.js @@ -206,7 +206,17 @@ export async function nonCriticalSideEffects ({ invoice, actIds }, { models }) { where: invoice ? { invoiceId: invoice.id } : { id: { in: actIds } }, include: { item: true } }) - notifyZapped({ models, item: itemAct.item }).catch(console.error) + // avoid duplicate notifications with the same zap amount + // by checking if there are any other pending acts on the item + const pendingActs = await models.itemAct.count({ + where: { + itemId: itemAct.itemId, + createdAt: { + gt: itemAct.createdAt + } + } + }) + if (pendingActs === 0) notifyZapped({ models, item: itemAct.item }).catch(console.error) } export async function onFail ({ invoice }, { tx }) { From 5261c83f4d79f2535d8af980260892b88aac20b8 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Wed, 8 Jan 2025 20:47:34 +0100 Subject: [PATCH 4/5] Add FAQ to version control (#1799) * Add FAQ to version control * Add script to deploy markdown content * Move faq.md into docs/user --- docs/{ => dev}/generating-seed.md | 0 docs/{ => dev}/semantic-search.md | 0 docs/{ => dev}/time-is-a-bitch.md | 0 docs/{ => dev}/useful-dev-commands.md | 0 docs/user/faq.md | 686 ++++++++++++++++++++++++++ scripts/deploy_user_documentation.js | 81 +++ 6 files changed, 767 insertions(+) rename docs/{ => dev}/generating-seed.md (100%) rename docs/{ => dev}/semantic-search.md (100%) rename docs/{ => dev}/time-is-a-bitch.md (100%) rename docs/{ => dev}/useful-dev-commands.md (100%) create mode 100644 docs/user/faq.md create mode 100644 scripts/deploy_user_documentation.js diff --git a/docs/generating-seed.md b/docs/dev/generating-seed.md similarity index 100% rename from docs/generating-seed.md rename to docs/dev/generating-seed.md diff --git a/docs/semantic-search.md b/docs/dev/semantic-search.md similarity index 100% rename from docs/semantic-search.md rename to docs/dev/semantic-search.md diff --git a/docs/time-is-a-bitch.md b/docs/dev/time-is-a-bitch.md similarity index 100% rename from docs/time-is-a-bitch.md rename to docs/dev/time-is-a-bitch.md diff --git a/docs/useful-dev-commands.md b/docs/dev/useful-dev-commands.md similarity index 100% rename from docs/useful-dev-commands.md rename to docs/dev/useful-dev-commands.md diff --git a/docs/user/faq.md b/docs/user/faq.md new file mode 100644 index 000000000..5aacdab2e --- /dev/null +++ b/docs/user/faq.md @@ -0,0 +1,686 @@ +--- +title: Frequently Asked Questions +id: 349 +sub: meta +--- + +# Stacker News FAQ + +To quickly browse through this FAQ page, click the chapters icon in the top-right corner. This will let you scroll through all FAQ chapter titles or search for a particular topic within this page. + +--- + +## New Stackers Start Here + +‎ +##### What is Stacker News? + +Stacker News is a forum (like Reddit or Hacker News) where you can earn sats for creating or curating content. Rather than collecting “upvotes” that are not redeemable or transferable on Reddit or Hacker News, on Stacker News you can earn sats. + +‎ +##### What Are Sats? + +Sats are the smallest denomination of Bitcoin. Just like there are 100 pennies in 1 dollar, there are 100,000,000 sats in 1 Bitcoin. On Stacker News, all Bitcoin payments and balances are denominated in sats. + +‎ +##### Do I Need Bitcoin to Use Stacker News? + +No. Every new stacker can comment for free (with limited visibility) while they earn their first few sats. After a stacker has started earning sats for their content, subsequent posts and comments will incur a small fee to prevent spam and to encourage quality contributions. Many stackers earn enough sats from their posts and comments to continue posting on the site indefinitely without ever depositing their own sats. + +Post and comment fees vary depending on the [territory](https://stacker.news/faq#stacker-news-territories). + +‎ +##### Why Is My Wallet Balance Going Up? + +When other stackers [zap](https://stacker.news/faq#zapping-on-stacker-news) your posts and comments, those sats go to you. Stackers who are actively contributing content and sats also earn extra sats as a daily reward. These sats come from the revenue generated by Stacker News from posting/commenting fees and boost fees. + +----- + +## Creating an Account + +‎ +##### How Do I Create a Stacker News Account? + +The most private way to create a Stacker News account is by logging in with one of the Lightning wallets listed below. + +Lightning wallets for logging in to Stacker News: + +- Alby +- Balance of Satoshis +- Blixt +- Breez +- Coinos +- LNbits +- LNtxbot +- Phoenix +- SeedAuth +- SeedAuthExtension +- SimpleBitcoinWallet +- ThunderHub +- Zap Desktop +- Zeus + +Alternatively, new stackers can set up an account by linking their email, Nostr, Github, or X accounts. + +‎ +##### How Do I Login With Lightning? + +To login with Lightning: + +1. Click [Login](/login) +2. Select [Login with Lightning](/login?type=lightning) +3. Open one of the Lightning wallets listed above +4. Scan the QR code that appears on Stacker News +5. Confirm your log in attempt on your Lightning wallet + +‎ +##### Can I Use Multiple Login Methods? + +Yes. + +Once you’re logged in, follow these steps to link other authentication methods: +1. Click your username +2. Click [settings](/settings) +3. Scroll down to link other authentication methods + +Once you’ve linked another authentication method to your account, you’ll be able to access your account on any device using any one of your linked authentication methods. + +‎ +##### Why Should I Log In With Lightning? + +Logging in with Lightning is the most private method of logging in to Stacker News. + +Rather than entering an email address, or linking your X or Github accounts, you can simply scan a QR code with your Lightning wallet or use a Lightning web wallet like Alby which enables desktop stackers to log in with a single click. + +‎ +##### How Do I Set a Stacker News Username? + +When setting up an account, Stacker News will automatically create a username for you. + +To change your username: + +1. Click your username (it's in the top-right corner of your screen) +2. Select profile +3. Click ‘edit nym’ + +--- + +## Funding Your Account + +‎ +##### How Do I Fund My Stacker News Wallet? + +There are three ways to fund your Stacker News account: + +1. By QR code +2. By Lightning Address +3. By sharing great content + +‎ +###### QR code + +1. Click your username +2. Click [wallet](/wallet) +3. Click fund +4. Enter a payment amount +5. Generate an invoice on Stacker News +6. Pay the invoice on your Lightning wallet + +‎ +###### Lightning Address + +1. Click your username +2. Open a wallet that offers Lightning Address support +3. Enter your Stacker News Lightning Address on your wallet +4. Pay any amount to fund your Stacker News account + +‎ +###### Sharing great content + +Every new stacker gets free comments (with limited visibility) to get started on Stacker News. Many stackers have earned enough sats from their first few posts and comments to continue posting on the site indefinitely without ever depositing their own sats. + +‎ +##### What Is a Lightning Address? + +A Lightning Address is just like an email address, but for your Bitcoin. + +It is a simple tool that anyone can use to send Bitcoin without scanning QR codes or copying and pasting invoices between wallets. + +For more on how Lightning Addresses work, [click here](https://lightningaddress.com/). + +‎ +##### Where Is My Stacker News Lightning Address? + +All stackers get Lightning addresses, which follow the format of username@stacker.news. + +Your Lightning address can also be found on your profile page, highlighted with a yellow button and a Lightning bolt icon. + +‎ +##### How Do I See My Account Balance? + +When logged in, your wallet balance is the number shown in the top-right corner of your screen. + +Clicking your wallet balance allows you to fund, withdraw, or view your past transactions. + +‎ +##### How Do I See My Transaction History? + +To see your full history of Stacker News transactions: + +1. Click your wallet balance in the top-right corner of your screen +2. Click [Wallet History](https://stacker.news/satistics?inc=invoice,withdrawal,stacked,spent) +3. Select which data you would like to see from the top menu + +The buttons on your wallet history page allow you to view and filter your past funding invoices, withdrawals, as well as the transactions where you stacked sats or spent sats on Stacker News. + +----- + +## Posting on Stacker News + +‎ +##### How Do I Post? + +To submit a post, click the Post button in the nav bar. + +Each post has a small fixed fee as a measure to limit spam, and to encourage stackers to post quality content. + +There are a few different types of posts stackers can make on Stacker News, including links, discussions, polls, and bounties. + +- Link posts require a title and a URL (stackers can optionally include a discussion prompt) +- Discussion posts require a title and a discussion prompt (stackers can optionally add links to their discussion prompt) +- Poll posts require a title and at least two poll options to choose from +- Bounty posts require a title, prompt, and a bounty amount to be paid on task completion + +‎ +##### How Do I Comment? + +To comment on a post: + +1. Click the title of the post you want to comment on +2. Submit your comment in the text box below the post + +To reply to a comment: + +1. Click reply beneath the comment you want to reply to +2. Submit your comment in the text box below the comment + +‎ +##### How Do Posting Fees Work? +Post and comment fees vary depending on a few factors. + +First, territory owners have the ability to set their own post and comment fees. + +Additionally, fees increase by 10x for repetitive posts and self-reply comments to prevent spam. + +As an example, if it costs 10 sats for a stacker to make a post in a territory, it will cost 100 sats if they make a second post within 10 minutes of their first post. If they post a third time within 10 minutes of their first one, it will cost 1,000 sats. + +This 10x fee escalation continues until 10 minutes have elapsed, and will reset to a fee of 10 sats when the stacker goes 10 minutes or more without posting or replying to themselves in a comment thread. + +This 10 minute fee escalation rule does not apply to stackers who are replying to other stackers, only those who repetitively post or reply to themselves within a single thread. + +There are also fees for uploads but your first 250 MB within 24 hours are free. After that, every upload will cost 10 sats until you reach 500 MB. Then the fee is raised to 100 sats until 1 GB after which every upload will cost 1,000 sats. After 24 hours, you can upload 250 MB for free again. Uploads without being logged in always cost 100 sats. + +Upload fees are applied when you submit your post or comment. Uploaded content that isn't used within 24 hours in a post or comment is deleted. + +‎ +##### What Is a Boost? + +Boosts allow stackers to increase the ranking of their post upon creation to give their content more visibility. + +‎ +##### How Do I Earn Sats on Stacker News? + +Stackers reward each other for their contributions by zapping them with sats. + +To start earning sats, you can share interesting links, discussion prompts, or comments with the community. + +Beyond the direct payments from other stackers, Stacker News also uses the revenue it generates from its job board, boost fees, post fees, and stacker donations to reward stackers that contributed to the site with even more sats. + +Every day, Stacker News rewards either creators or zappers with a daily reward. These rewards go to stackers who either created or zapped one or more of the top 33% of posts and comments from the previous day. The rewards scale with the ranking of the content as determined by other stackers. + +Finally, Stacker News also rewards stackers with sats for referring new stackers to the platform. To read more about the Stacker News referral program, click [here](https://stacker.news/items/349#how-does-the-stacker-news-referral-program-work). + +‎ +##### How Do I Format Posts on Stacker News? + +Stacker News uses [github flavored markdown](https://guides.github.com/features/mastering-markdown/) for styling all posts and comments. + +You can use any of the following elements in your content: + +- Headings +- Blockquotes +- Unordered Lists +- Ordered Lists +- Inline code with syntax highlighting +- Tables +- Text Links +- Line Breaks +- Subscript or Superscript + +In addition, stackers can tag other stackers with the @ symbol like this: @sn. Stackers can also refer to different territories with the ~ symbol like this: ~jobs. + +‎ +##### How Do I Post Images or Videos on Stacker News? + +There are two ways to post images or videos: + +1. By pasting a URL to an image or video +2. By uploading an image or video + +If you have a URL, you can simply paste it into any textbox. Once your link is pasted into the textbox of a post or comment, it will automatically be rendered as an image or video when you preview or post. + +To upload files, click the upload icon on the top-right corner of the textbox. This will open a file explorer where you can select the files you want to upload (or multiple). We currently support following file types: + +- image/gif +- image/heic +- image/png +- image/jpeg +- image/webp +- video/mp4 +- video/mpeg +- video/webm + +Uploaded content that isn't used within 24 hours in a SN post or comment is deleted. + +As explained in the [section about posting fees](https://stacker.news/faq#how-do-posting-fees-work), fees might apply for uploads. + +To expand an image on Stacker News, click the image. Clicking it again will shrink it back to its original size. + +If you are trying to post images from Twitter on Stacker News, make sure you have selected the tweet's image URL, and not the tweet URL itself. + +To find the image URL of a twitter photo, right-click the image on Twitter, select "Open In New Tab", and copy that URL. + +‎ +##### Stacker News Shortcuts + +Stacker News supports a handful of useful keyboard shortcuts for saving time when creating content: + +`ctrl+enter`: submit any post/comment/form +`ctrl+k`: link in markdown fields +`ctrl+i`: italics in markdown fields +`ctrl+b`: bold in markdown fields +`ctrl+alt+tab`: real tab in markdown fields + +----- + +## Stacker News Territories + +‎ +##### What are Territories? + +Territories are communities on Stacker News. Each territory has an owner who acts as a steward of the community, and anyone can post content to the territory that best fits the topic of their post. + +When Stacker News first launched, much of the discussion focused exclusively on Bitcoin. However, the launch of territories means anyone can now create a thriving community on Stacker News to discuss any topic. + +‎ +##### Can Anyone Start a Territory? + +Anyone can start a territory by clicking the dropdown menu next to the logo on the homepage, scrolling to the bottom of the list, and clicking [create](https://stacker.news/territory). Stackers can also create as many territories as they want. + +‎ +##### How Much Does It Cost to Start a Territory? + +Starting a territory costs either 100k sats/month, 1m sats/year, or 3m sats as a one-time payment. + +If a territory owners chooses either the monthly or yearly payment options, they can select 'auto-renew' so that Stacker News is automatically paid the territory fee each month or year. If a territory owner doesn't select 'auto-renew', they will get a notification to pay an invoice within 5 days after the end of their month or year to keep their territory. + +If you later change your mind, your payment for the current period is included in the new cost. This means that if you go from monthly to yearly payments for example, we will charge you 900k instead of 1m sats. + +‎ +##### Can Territory Owners Earn Sats? + +Yes, territory owners earn 70% of all fees generated by content in their specific territory. This means territory owners earn 7% of all sats zapped within their territory, as well as 70% of all sats paid as boosts or posting and commenting costs within their territory. These rewards are paid to territory owners each day as part of the Stacker News daily rewards. + +The remaining 30% of fees generated by content in a given territory is paid to the Stacker News daily rewards pool, which rewards the best contributors on the site each day. + +‎ +##### What Variables Do Territory Owners Control? + +Territory owners can set the following variables for their territory: + +- Territory name +- Territory description +- Minimum posting cost +- Allowable post types + +Territory owners can also mark their territory as NSFW or enable moderation. Moderation allows them to outlaw content with one click (see [How Do I Flag Content](https://stacker.news/faq#how-do-i-flag-content-i-dont-like)). + +All territory variables can be updated after creation. + +‎ +##### What Happens If I No Longer Want My Territory? + +If a territory owner chooses not to renew their territory at the end of their billing period, the territory will be archived. Stackers can still see archived posts and comments, but they will not be able to create new posts or comments until someone takes ownership of the territory. + +----- + +## Discovering Content on Stacker News + +‎ +##### How Do I Search on Stacker News? + +To search for content on Stacker News, click the magnifying glass located in the navbar. This is a powerful feature that allows stackers to search for posts, comments, and other stackers across the site. + +[Search results](https://stacker.news/search) can be filtered by the following metrics: + +- best match +- most recent +- most comments +- most sats +- most votes + +In addition, search results can be segmented over time, showing the relevant results from the past day, week, month, year, or forever. + +Finally, there are some hidden search commands that can further assist you with identifying specific types of content on Stacker News: + +`~territoryname` allows you to search within a specific territory +`nym:ausersnym` allows you to search for items from a certain user by replacing `ausernym` with the nym you want to find +`url:aurl` allows you to search for certain domain names by replacing `aurl` with a domain name you want to find + +‎ +##### How Do I Subscribe to Someone on Stacker News? + +If you find a stacker you want to see more content from, you can click their profile and then click the `...` icon next to their photo. There, you can choose to either subscribe to their posts or their comments. + +Once subscribed, you'll get a notification each time they post content. + +‎ +##### How Do I Subscribe to Posts on Stacker News? + +If you find a post you want to follow along with, click the `...` icon next to the post metadata and select subscribe. + +Once subscribed, you'll get a notification each time someone makes a comment on that post. + +‎ +##### How Do I Mute on Stacker News? + +If you want to mute a stacker, click the `...` icon next to one of their posts or the `...` icon on their profile page and select mute. + +Once muted, you'll no longer see that stacker's content or get notified if they comment on your content. + +‎ +##### How Do I Find New Territories on Stacker News? + +Stacker News offers a number of territories, or topic-based collections of content. + +To explore a particular territory on Stacker News, click the dropdown menu next to the Stacker News logo in the navbar and select the topic you'd like to see content on. + +If you want to post content to a particular territory, the territory you're currently browsing will automatically be selected as the territory for your post. + +If you wish to post your content in a different territory, simply select a new one from the dropdown on the post page and fill out your post details there. + +----- + +## Zapping on Stacker News + +‎ +##### How Do I Zap on Stacker News? + +To send a zap, click the Lightning bolt next to a post or comment. Each click will automatically send your default zap amount to the creator of the post or comment. You can zap a post or comment an unlimited number of times. + +You can also zap any specific number of sats by either changing your default zap amount or by setting a custom zap amount on an individual piece of content. + +‎ +##### How Do I Change My Default Zap Amount? + +You can change your default zap amount in your settings: + +1. Click your username +2. Click [settings](/settings) +3. Enter a new default zap amount + +‎ +##### How Do I Zap a Custom Amount? + +To send a custom zap amount, long-press on the Lightning bolt next to a post or comment until a textbox appears. Then type the number of sats you’d like to zap, and click zap. + +‎ +##### Turbo Zaps + +Turbo Zaps is an opt-in, experimental feature for improving zapping UX. When enabled in your settings, every Lightning bolt click on a specific post or comment raises your total zap to the next 10x of your default zap amount. If your default zap amount is 1 sat: + +- your first click: 1 sat total zapped +- your second click: 10 sats total zapped +- your third click: 100 sats total zapped +- your fourth click: 1000 sats total zapped +- and so on... + +Turbo zaps only escalate your zapping amount when you repeatedly click on the Lightning bolt of a specific post or comment. Zapping a new post or comment will once again start at your default zap amount, and escalate by 10x with every additional click. + +Turbo zaps is a convenient way to modify your zap amounts on the go, rather than relying on a single default amount or a long-press of the Lightning bolt for all your zapping. + +‎ +##### Do Zaps Help Content Rank Higher? + +Yes. The ranking of an item is affected by: + +- the amount a stacker zaps a post or comment +- the trust of the stacker making the zap +- the time elapsed since the creation of the item + +Zapping an item with more sats amplifies your trust, giving you more influence on an item's ranking. However, the relationship between sats contributed and a stacker's influence on item ranking is not linear, it's logarithmic. + +The effect a stacker's zap has on an item's ranking is `trust*log10(total zap amount)` where 10 sats = 1 vote, 100 sats = 2, 1000 sats = 3, and so on ... all values in between are valid as well. + +To make this feature sybil resistant, SN takes 30% of zaps and re-distributes them to territory founders and the SN community as part of the daily rewards. + +‎ +##### Why Should I Zap Posts on Stacker News? + +There are a few reasons to zap posts on Stacker News: + +1. To influence the ranking of content on the site + +Every post and comment is ranked based on the number of people who zapped it and the trust level of each zapping stacker. More zaps from more trusted stackers means more people will see a particular piece of content. + +2. To acknowledge the value of the content other people create (value for value) + +Sending someone a like or an upvote incurs no cost to you, and therefore these metrics can easily be gamed by bots. Sending someone sats incurs a direct cost to you, which gives the recipient a meaningful reward and acts as a clear signal that you found a particular piece of content to be valuable. + +3. To earn trust for identifying good content + +On Stacker News, new stackers start with zero trust and either earn trust by zapping good content or lose trust by zapping bad content. + +‎ +##### Can I Donate Sats to Stacker News? + +Yes. Every day, Stacker News distributes the revenue it collects from job listings, posting fees, boosts, and donations back to the stackers who made the best contributions on a given day. + +To donate sats directly to the Stacker News rewards pool, or to view the rewards that will be distributed to stackers tomorrow, [click here](https://stacker.news/rewards). + + +----- + +## Job Board + +‎ +##### How Do I Post a Job on Stacker News? + +To post a job on Stacker News: + +1. Navigate to the ~jobs territory +2. Click post + +Fill out all the details of your job listing, including: + +- Job title +- Company name +- Location +- Description +- Application URL or email + +If you wish to promote your job, you can also set a budget for your job listing. + +All promoted jobs are paid for on a sats per minute basis, though you can also see an expected monthly USD price when you set your budget. + +Your budget determines how highly your job listing will rank against other promoted jobs on the Stacker News job board. + +If you want to get more people viewing your job, consider raising your budget above the rate that other employers are paying for their listings. + +If you choose not to promote your job, your listing will be shown in reverse-chronological order, and will be pushed down the job board as new listings appear on Stacker News. + +‎ +##### How Are Job Listings Ranked on Stacker News? + +Each job is listed in reverse-chronological order on Stacker News, with an option for employers to pay a promotion fee to maintain the ranking of their job listing over time. + +For employers who choose to promote their jobs, the fee amount determines the ranking of a job. The more an employer is willing to pay to advertise their job, the higher their listing will rank. + +If two jobs have identical fees, the first job that was posted will rank higher than the more recent one. + +‎ +##### Where Do Job Posting Fees Go? + +Stacker News earns revenue from job posting fees, as well as boosts, post and comment fees, and a fee on all zaps on the platform. All of that revenue is then paid back to stackers as daily rewards. + +The sats from the daily rewards go to the stackers who contribute posts and comments each day. + +----- + +## Ranking & Influence on Stacker News + +‎ +##### What Does The Lightning Bolt Button Do? + +The lightning bolt button next to each post and comment is a tool for stackers to signal that they like what they see. + +The big difference between the Stacker News lightning bolt and the "like" or "upvote" buttons you might find on other sites is that when you press the lightning bolt you're not only raising the ranking of that content, you're also zapping the stacker who created the content with your sats. + +- A grey lightning bolt icon means you haven't zapped the post or comment yet +- A colored lightning bolt icon means you have zapped the post or comment (the color changes depending on how much you zap, and you can zap as many times as you like) +- If there is no lightning bolt next to a post or comment it means you created the content, and therefore can't zap it + +‎ +##### How Does Stacker News Rank Content? + +Stacker News uses sats alongside a Web of Trust to rank content and deter Sybil attacks. + +As [explained here](https://stacker.news/items/349#do-zaps-help-content-rank-higher), stackers can send zaps to each other by clicking the lightning bolt next to a post or comment. The zap amounts are one factor that helps determine which content ranks highest on the site, and are weighted by how much the stacker sending the zap is trusted. + +The Stacker News ranking algorithm works as follows: + +- The number of stackers who have zapped an item +- Multiplied by the product of the trust score of each stacker and the log value of sats zapped +- Divided by a power of the time since a story was submitted +- Plus the boost divided by a larger power (relative to un-boosted ranking) of the time since a story was submitted + +The comments made within a post are ranked the same way as top-level Stacker News posts. + +‎ +##### How Does The Stacker News Web of Trust Work? + +Each stacker has a trust score on Stacker News. New accounts start without any trust, and over time stackers can earn trust by zapping good content, and lose trust by zapping bad content. + +The only consideration that factors into a stacker’s trust level is whether or not they are zapping good content. The zap amount does not impact a stacker's trust. + +In addition, stackers do not lose or gain trust for making posts or comments. Instead, the post and comment fees are the mechanism that incentivizes stackers to only make high quality posts and comments. + +A stacker’s trust is an important factor in determining how much influence their zaps have on the ranking of content, and how much they earn from the daily sat reward pool paid to zappers as [explained here](https://stacker.news/items/349#why-should-i-zap-posts-on-stacker-news). + +‎ +##### How Do I Flag Content I Don't Like? + +If you see content you don't like, you can click the `...` next to the post or comment to flag it. This is a form of negative feedback that helps Stacker News decide which content should be visible on the site. + +It costs 1 sat to flag content, and doing so doesn't affect your trust or the trust of the stacker who posted the content. Instead, it simply lowers the visibility of the specific item for all stackers on Tenderfoot mode. + +If an item gets flagged by stackers with enough combined trust, it is outlawed and hidden from view for stackers on Tenderfoot mode. If you wish to see this flagged content without any modifications, you can enable Wild West mode in your settings. + +‎ +##### What is Tenderfoot Mode? + +Tenderfoot mode hides or lowers the visibility of flagged content on Stacker News. This is the default setting for all stackers. + +‎ +##### What is Wild West Mode? + +Wild West mode allows you to see all content on Stacker News, including content that has been flagged by stackers. + +This unfiltered view doesn't modify the visibility of items on Stacker News based on negative feedback from stackers. + +You can enable Wild West mode in your settings panel. + +‎ +##### What is sats filter? + +Sats filter allows you to choose how many sats have been "invested" in a post or content for you to see it. "Invested" sats are the sum of posting costs, zapped sats, and boost. + +If you'd like to see all content regardless of investment, set your sats filter to 0. + +----- + +## Notification Settings + +‎ +##### Where Are My Stacker News Notifications? + +To see your notifications, click the bell icon in the top-right corner of the screen. A red dot next to the bell icon indicates a new notification. + +To change your notification settings: + +1. Click your username +2. Click [settings](/settings) +3. Update your preferences from the ‘Notify me when…’ section + +‎ +##### How Do I Create A Bio on Stacker News? + +To fill out your bio: + +1. Click your username +2. Click profile +3. Click edit bio + +‎ +##### How Do I View My Past Stacker News Transactions? + +To view your transaction history: + +1. Click your [wallet balance](/wallet) next to your username +2. Click wallet history + +----- + +## Other FAQs + +‎ +##### How Does The Stacker News Referral Program Work? + +For every new stacker you refer, you'll receive: + +- 2.1% of all the sats they earn for their content +- 21% of all the sats they spend on boosts or job listings + +Any Stacker News link can be turned into a referral link by appending /r/, e.g. `/r/k00b` to the link. This means you can earn sats for sharing Stacker News links on any website, newsletter, video, social media post, or podcast. + +Some examples of referral links using @k00b as an example include: + +`https://stacker.news/r/k00b` +`https://stacker.news/items/109473/r/k00b` +`https://stacker.news/top/posts/r/k00b?when=week` + +To make referring stackers easy, every post also has a link sharing button in the upper right corner. If you are logged in, copying the link will automatically add your referral code to it. + +For logged in stackers, there is a [dashboard](https://stacker.news/referrals/month) to track your referrals and how much you're earning from them. It's available in the dropdown in the navbar. + +The money paid out to those who refer new stackers comes out of SN's revenue. The referee doesn't pay anything extra, the referrer just gets extra sats as a reward from SN. + +‎ +##### Where Should I Submit Feature Requests? +Ideally on the git repo https://github.com/stackernews/stacker.news/issues. The more background you give on your feature request the better. The hardest part of developing a feature is understanding the problem it solves, all the things that can wrong, etc. + +‎ +##### Will Stacker News Pay For Contributions? +Yes, we pay sats for PRs. Sats will be proportional to the impact of the PR. If there's something you'd like to work on, suggest how much you'd do it for on the issue. If there's something you'd like to work on that isn't already an issue, whether its a bug fix or a new feature, create one. + +‎ +##### Where Should I Submit Bug Reports? +Bug reports can be submitted on our git repo: https://github.com/stackernews/stacker.news/issues. + +‎ +##### Responsible Disclosure +If you find a vulnerability on Stacker News, we would greatly appreciate it if you contact us via hello@stacker.news or [t.me/k00bideh](https://t.me/k00bideh). + +‎ +##### Where Can I Ask More Questions? +Reply to this FAQ. It's like any other post on the site. \ No newline at end of file diff --git a/scripts/deploy_user_documentation.js b/scripts/deploy_user_documentation.js new file mode 100644 index 000000000..b44ee947d --- /dev/null +++ b/scripts/deploy_user_documentation.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +const path = require('path') +const fs = require('fs') + +const SN_API_URL = process.env.SN_API_URL ?? 'http://localhost:3000' +const SN_API_KEY = process.env.SN_API_KEY + +const parseFrontMatter = (content) => { + const lines = content.split('\n') + if (lines[0] !== '---') { + throw new Error('failed to parse front matter: start delimiter not found') + } + + const endIndex = lines.findIndex((line, i) => i > 0 && line === '---') + if (endIndex === -1) { + throw new Error('failed to parse front matter: end delimiter not found') + } + + const meta = {} + for (let i = 1; i < endIndex; i++) { + const line = lines[i] + const [key, ...valueParts] = line.split(':') + if (key && valueParts.length) { + meta[key.trim()] = valueParts.join(':').trim() + } + } + + return meta +} + +const readItem = (name) => { + const content = fs.readFileSync(path.join(__dirname, name), 'utf8') + const lines = content.split('\n') + const startIndex = lines.findIndex((line, i) => i > 0 && line.startsWith('---')) + 1 + return { + ...parseFrontMatter(content), + text: lines.slice(startIndex).join('\n') + } +} + +async function upsertDiscussion (variables) { + if (!SN_API_KEY) { + throw new Error('SN_API_KEY is not set') + } + + const response = await fetch(`${SN_API_URL}/api/graphql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': SN_API_KEY + }, + body: JSON.stringify({ + query: ` + mutation upsertDiscussion($id: ID!, $sub: String!, $title: String!, $text: String!) { + upsertDiscussion(id: $id, sub: $sub, title: $title, text: $text) { + result { + id + } + } + } + `, + variables + }) + }) + + if (response.status !== 200) { + throw new Error(`failed to upsert discussion: ${response.statusText}`) + } + + const json = await response.json() + if (json.errors) { + throw new Error(json.errors[0].message) + } + + return json.data +} + +const faq = readItem('../docs/user/faq.md') + +upsertDiscussion(faq) From 7daf688ea3a6b18ab211665a971fb8d0f1052d31 Mon Sep 17 00:00:00 2001 From: k00b Date: Wed, 8 Jan 2025 18:10:14 -0600 Subject: [PATCH 5/5] remove p2p zap notification indicator fixing 'phantom' notifications --- api/resolvers/user.js | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 34e2aa402..f131fa929 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -441,26 +441,6 @@ export default { } if (user.noteWithdrawals) { - const p2pZap = await models.invoice.findFirst({ - where: { - confirmedAt: { - gt: lastChecked - }, - invoiceForward: { - withdrawl: { - userId: me.id, - status: 'CONFIRMED', - updatedAt: { - gt: lastChecked - } - } - } - } - }) - if (p2pZap) { - foundNotes() - return true - } const wdrwl = await models.withdrawl.findFirst({ where: { userId: me.id,