Conversation
…ting Anchored text comments with highlights, one-level replies, org-scoped resolve/reopen, named anonymous comments behind optional invisible reCAPTCHA v2 and the IP throttler, and a dashboard restyle of /p/:id.
…omments # Conflicts: # apps/frontend/src/app/(app)/layout.tsx # libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
…comments feat(preview): inline comments, replies, resolve and anonymous commenting
…e silent-video audio hint The container status poll is HTTP 200 with the failure in the body, so it never went through handleErrors and Meta's raw status text reached the tooltip.
…o a disconnect error
…audio-tooltip-main fix(instagram): curate container processing errors, map 2207082 to the silent-video audio hint
…allbacks The STORE_REFRESH_TOKEN and REMOVE_REFRESH_TOKEN calls to the Postiz browser extension passed a no-op callback. When the extension is not installed, disabled, or the origin is not allowed, Chrome logs "Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist." to the user's console because nothing read lastError. Reading it in the callback, as add.provider.component.tsx already does, marks the error as checked and silences the log. The calls stay fire-and-forget with no behavior change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rrors A Telegram 400 (caption too long, media Telegram could not download) is a permanent rejection, but it surfaced as a plain error the workflow retried 3 times and then reported as "Could not publish after several attempts" with a misleading couldn't-confirm email. Map it to BadBody so it fails once with the real reason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PyDRJpL27E7WCK5UGsUope
finalizeEvent requires the secret key as a Uint8Array, but post/comment passed the stored hex string, so every Nostr publish crashed with "expected Uint8Array, got type=string". Reuse the same conversion authenticate already does via a shared helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PyDRJpL27E7WCK5UGsUope
…ipt errors MetaMask's injected inpage.js throws "Failed to connect to MetaMask" (linked: "MetaMask extension not found") as Error instances, so the existing plain-object wallet filter does not catch them and beforeSend opens the user-report dialog. Both messages are added to ignorePatterns. Sentry CLOUD-C: 860 events / 166 users in the last 30 days, 2270 events / 305 users lifetime, no Postiz frames in the stack. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y-main fix(telegram): map Telegram 400 rejections to non-retryable BadBody errors
…tamask fix(frontend): stop the Sentry report dialog on MetaMask injected-script errors
A page listed without an access_token made reConnect return an undefined token, and the integration upsert then threw a 500.
…-main fix(nostr): convert the hex private key to bytes before signing posts
…ked-last-error fix(frontend): silence Unchecked runtime.lastError from extension messaging callbacks
…ct-page-token fix(facebook): fail reconnect cleanly when the page has no page token
…ed file The Uppy upload-success handler read getState().files[file.id].progress unconditionally. When the file had already been removed from Uppy's state (validation or error paths call removeFile) the lookup is undefined and the handler threw "Cannot read properties of undefined (reading 'progress')", which also pops the Sentry report dialog. Read the file state once and return when it is gone; setFileState is otherwise unchanged. Sentry CLOUD-20X + CLOUD-211 (Safari) + CLOUD-20Y: 1375 events / 62 users in the last 30 days, first seen 2026-09-16. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
getPostsByGroup crashed on posts[0].integrationId when the group had no posts left (deleted from another tab, by a teammate, or on sub cancel), so the API returned a 500 body. The calendar's editPost then indexed data.posts[0] on that error body and threw an unhandled TypeError, which also pops the Sentry report dialog. The service now throws NotFoundException like the other post lookups, and editPost returns early with the existing "Post not found" toast and a calendar mutate so the stale tile disappears. Sentry CLOUD-QF (Chrome) + CLOUD-QK (Safari): 833 events / 309 users in the last 30 days, 2807 / 635 lifetime. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ess error The @uppy/aws-s3 4.3.2 progress callback reads uppy.getFile(id).progress from the S3 part PUT load handler without a null check, so when the file has already left uppy's state it throws "undefined is not an object (evaluating 'r.progress')" (Sentry CLOUD-211). beforeSend shows the report dialog for every captured exception, so the throw surfaced as a "Something broke" popup for users on Safari. The upload itself is done or cancelled by the time this fires, so the error is ignored like the posthog recorder one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…py-progress fix(frontend): stop the Sentry report dialog on the uppy aws-s3 progress error
…-file-progress fix(media): skip the upload-success state update for an already removed file
…-group-guard fix(calendar): handle a deleted post group when opening a calendar tile
…heck add.provider.component.tsx tested chrome?.runtime?.sendMessage without first checking typeof chrome, so on Safari and iOS Chrome, where the chrome global does not exist, the bare identifier threw "ReferenceError: Can't find variable: chrome" instead of showing the existing "Extension Not Found" modal. The other two extension call sites already guard with typeof chrome !== 'undefined'. Sentry CLOUD-14W: 8 events / 6 users in the last 30 days, 22 / 11 lifetime, mostly on the LinkedIn connect flow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y state The Uppy error handler called uppy.clear() while the failed upload was still registered in currentUploads. Uppy core throws "The installed uploader plugin does not allow removing files during an upload." from clear() in that state, so every Transloadit assembly_error produced a second, unhandled exception on top of the real failure and popped the Sentry report dialog. cancelAll() removes every file first, which drops the upload record before that check, then resets the upload state; the rest of the handler is unchanged. Sentry CLOUD-S1: 53 events / 29 users in the last 30 days, 264 / 75 lifetime. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…crashing loadStripe() rejects when js.stripe.com is blocked (ad blockers, privacy extensions, corporate proxies). The promise was stored in state with no handler, so the rejection surfaced as an unhandled "Error: Failed to load Stripe.js", popped the Sentry report dialog, and the billing page stayed on the loading spinner forever. The promise now has a catch that flips a stripeFailed state, and the page renders an explanation in the same bordered box used for the "another account already subscribed" case. New translation key billing_stripe_load_failed added to all locales via lingo.dev. Sentry CLOUD-SJ: 185 events / 27 users in the last 30 days, 338 / 75 lifetime. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-undefined-guard fix(frontend): guard the chrome global before the extension connect check
…on-error fix(media): cancel in-flight uploads on error instead of clearing Uppy state
…ror-main fix: friendly message when Threads reports an UNKNOWN container error
…redirect started On a successful activation the backend answers with the onboarding header. The shared afterRequest handler sees it, sets window.location to the onboarding page and hands the response back; the activation page then called .json() on it while the page was already navigating away. Firefox rejects that body read with "AbortError: The operation was aborted.", which is unhandled and pops the Sentry report dialog. The page now returns early when the response carries the onboarding header and only reads the body otherwise, so the "already activated" path is unchanged. afterRequest itself is left alone because the register flow relies on getting the onboarding response back to fire its conversion events. Sentry CLOUD-11: 65 events / 65 users in the last 30 days, 311 / 132 lifetime, all Firefox on /auth/activate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…apping-main fix(instagram): map container error 2207085 to a curated video-format message
…oad-failure fix(billing): show a message when Stripe.js fails to load instead of crashing
…dy-on-redirect fix(auth): don't read the activate response body once the onboarding redirect started
Preview comments (inline, replies, resolve, anonymous + optional reCAPTCHA), connected clients, schedule post preview url, plus fixes: Instagram/Threads container error mapping, Telegram 400 -> BadBody, media upload cancel-on-error, calendar deleted post group, Stripe.js load failure message, extension chrome guards, nostr hex key signing. Conflicts resolved: - .env.example: union (Crove telemetry block + upstream reCAPTCHA and provider visibility docs; REDDIT_* stays removed) - first.billing.component: keep the DOS checkout branch (shared billing) and add upstream's stripeFailed branch; Stripe only loads outside shared dos billing - variable.context: keep brandConfig, add recaptchaSiteKey Schema: Comments gains threading/anchor/resolved columns and a nullable userId (anonymous). Beta DB is altered at deploy; the shared prod DB goes through the DOS-Me migration ledger before the prod deploy.
merge: upstream main (42 commits - preview comments, connected clients, platform fixes)
| return; | ||
| } | ||
| ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||
| setFlash(true); |
| const toggleResolved = useCallback(async () => { | ||
| const response = await fetch(`/posts/comments/${comment.id}/resolve`, { | ||
| method: 'PUT', | ||
| body: JSON.stringify({ resolved: !resolved }), | ||
| }); | ||
| if (!response.ok) { | ||
| toast.show( | ||
| t('preview_comment_failed', 'Could not post the comment'), | ||
| 'warning' | ||
| ); | ||
| return; | ||
| } | ||
| mutate(); | ||
| }, [comment.id, resolved]); |
| const toggleResolved = useCallback(async () => { | ||
| const response = await fetch(`/posts/comments/${comment.id}/resolve`, { | ||
| method: 'PUT', | ||
| body: JSON.stringify({ resolved: !resolved }), | ||
| }); | ||
| if (!response.ok) { | ||
| toast.show( | ||
| t('preview_comment_failed', 'Could not post the comment'), | ||
| 'warning' | ||
| ); | ||
| return; | ||
| } | ||
| mutate(); | ||
| }, [comment.id, resolved]); |
| const toggleResolved = useCallback(async () => { | ||
| const response = await fetch(`/posts/comments/${comment.id}/resolve`, { | ||
| method: 'PUT', | ||
| body: JSON.stringify({ resolved: !resolved }), | ||
| }); | ||
| if (!response.ok) { | ||
| toast.show( | ||
| t('preview_comment_failed', 'Could not post the comment'), | ||
| 'warning' | ||
| ); | ||
| return; | ||
| } | ||
| mutate(); | ||
| }, [comment.id, resolved]); |
| const toggleResolved = useCallback(async () => { | ||
| const response = await fetch(`/posts/comments/${comment.id}/resolve`, { | ||
| method: 'PUT', | ||
| body: JSON.stringify({ resolved: !resolved }), | ||
| }); | ||
| if (!response.ok) { | ||
| toast.show( | ||
| t('preview_comment_failed', 'Could not post the comment'), | ||
| 'warning' | ||
| ); | ||
| return; | ||
| } | ||
| mutate(); | ||
| }, [comment.id, resolved]); |
| const load = useCallback(async () => { | ||
| return (await fetch(`/public/posts/${previewId}/comments`)).json(); | ||
| }, [previewId]); |
| ), | ||
| }); | ||
| }), | ||
| [t] |
| setLoading(false); | ||
| } | ||
| }, | ||
| [user?.id, recaptchaSiteKey, previewId, parentId, anchor, askForName] |
| return; | ||
| } | ||
| mutate(); | ||
| }, [comment.id, resolved]); |
| const fetch = useFetch(); | ||
| const load = useCallback(async () => { | ||
| return (await fetch(`/public/posts/${previewId}/comments`)).json(); | ||
| }, [previewId]); |
There was a problem hiding this comment.
Code Review
This pull request introduces a robust inline and public commenting system for post previews, complete with text-anchored comments, replies, resolution states, and reCAPTCHA protection. It also adds connected client statistics to the admin dashboard, improves error handling across various social media providers (Facebook, Instagram, Nostr, Telegram, Threads), and updates internationalization files. The review feedback highlights two important backend improvements in posts.service.ts: wrapping the external reCAPTCHA verification network request in a try-catch block to prevent unhandled 500 errors, and batching database queries to avoid an N+1 performance bottleneck when detaching stale anchors inside a loop.
| const result = await ( | ||
| await fetch('https://www.google.com/recaptcha/api/siteverify', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
| body: new URLSearchParams({ | ||
| secret: process.env.RECAPTCHA_SECRET_KEY, | ||
| response: token, | ||
| remoteip: ip, | ||
| }), | ||
| }) | ||
| ).json(); | ||
|
|
||
| if (!result?.success) { | ||
| throw new BadRequestException('Captcha verification failed'); | ||
| } |
There was a problem hiding this comment.
The fetch call to Google's reCAPTCHA API is not wrapped in a try-catch block. If the external API is down or a network/DNS error occurs, fetch will throw a TypeError, resulting in an unhandled 500 Internal Server Error. Wrapping the network request in a try-catch block and throwing a BadRequestException ensures robust error handling.
try {
const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: process.env.RECAPTCHA_SECRET_KEY,
response: token,
remoteip: ip,
}),
});
const result = await response.json();
if (!result?.success) {
throw new BadRequestException('Captcha verification failed');
}
} catch (error) {
if (error instanceof BadRequestException) {
throw error;
}
throw new BadRequestException('Captcha verification failed due to a network error');
}| for (const post of posts) { | ||
| const anchored = await this._postRepository.getAnchoredCommentsForPost( | ||
| post.id | ||
| ); |
|
ESLint code-scanning failure: same alert set accepted on #65 (react-compiler style rules + no-img-element in upstream's new preview-comments/auth files; the repo's own eslint.yml gate passes; merge-introduced files lint clean). Accepted per the documented precedent - this promotion carries exactly the #65 diff to prod. |
Promotion for prod deploy: #65 (upstream sync 42 commits). PROD DB prerequisite already done: the Comments migration landed in the DOS-Me ledger and is verified live on the shared prod Supabase (6 new columns, nullable userId, indexes + FK, resolvedAt as timestamp(3) per Prisma model). Same reviewed diff, no new commits.
📌 TL;DR
This PR introduces a robust inline commenting system for post previews, allowing both authenticated and anonymous users to leave anchored comments with optional reCAPTCHA protection. It also refactors the preview UI to use a new design system, adds provider migration/visibility controls, and improves error handling for Stripe and Chrome extension interactions.
🎯 Type of Change
🔍 Changes Walkthrough
.env.exampleHIDDEN_PROVIDERS,MIGRATE_PROVIDERS)..zcodeignore.gitignoreand adding specific exclusions for build artifacts and secrets.apps/backend/src/api/routes/posts.controller.tscreateCommentto accept a structured DTO and IP address for public comments. Added a newresolveCommentendpoint to mark comments as resolved.apps/backend/src/api/routes/public.controller.tsPOST /posts/:id/commentsto allow anonymous users to submit comments, passing the real IP for security checks.apps/frontend/src/app/(app)/(preview)/p/[id]/layout.tsxbg-newBgColor,text-newTextColor).apps/frontend/src/app/(app)/(preview)/p/[id]/page.tsxPreviewCommentsProvider. Replaced static HTML rendering withPostContentClientto support text selection for comments. Updated UI structure to use new design tokens and sticky comment sidebar.apps/frontend/src/app/(app)/layout.tsxrecaptchaSiteKeyfrom environment variables to the frontend context.apps/frontend/src/app/global.scss.preview-comment-cursorwith a custom SVG cursor to indicate text selection for commenting.apps/frontend/src/components/admin/admin-stats.component.tsxapps/frontend/src/components/auth/after.activate.tsxonboardingheader and skip further processing if present.apps/frontend/src/components/billing/first.billing.component.tsxapps/frontend/src/components/launches/add.provider.component.tsxchromeobject is undefined in non-extension environments.apps/frontend/src/components/launches/calendar.tsxapps/frontend/src/components/launches/continue.integration.tsxlastErrorgracefully.apps/frontend/src/components/launches/menu/menu.tsxlastErrorgracefully.apps/frontend/src/components/media/new.uploader.tsxcancelAllinstead ofclearon error and safely accessing file state.apps/frontend/src/components/preview/comments.components.tsx1.
PreviewCommentsProvidercontext integration.2. Anonymous user support with name capture and reCAPTCHA v2 invisible integration.
3. Text anchoring (start/end/quote) for inline comments.
4. Threaded replies and "Resolve" functionality.
5. New UI components for composer, thread cards, and reviewer name modal.
📊 Architectural Flow
sequenceDiagram participant User as User (Browser) participant Frontend as Preview Page (React) participant Context as PreviewCommentsContext participant Backend as Backend API participant Recaptcha as Google reCAPTCHA User->>Frontend: Selects text in post content Frontend->>Context: setPending({start, end, quote}) Frontend->>User: Shows Comment Composer with anchor User->>Frontend: Submits Comment alt User is Authenticated Frontend->>Backend: POST /posts/:id/comments (with auth) else User is Anonymous Frontend->>Frontend: Prompt for Name (if not saved) Frontend->>Recaptcha: Execute Invisible reCAPTCHA Recaptcha-->>Frontend: Token Frontend->>Backend: POST /public/posts/:id/comments (with IP, Name, Token) end Backend->>Backend: Validate & Save Comment Backend-->>Frontend: Success Response Frontend->>Context: mutate() (Refetch comments) Context-->>Frontend: Updated Comment List Frontend->>User: Renders new comment thread