Skip to content

merge: upstream main (42 commits - preview comments, connected clients, platform fixes) - #65

Merged
JOY (JOY) merged 43 commits into
devfrom
upstream-sync-20260926
Sep 26, 2026
Merged

JOY (JOY) merged 43 commits into
devfrom
upstream-sync-20260926

Conversation

@JOY

@JOY JOY (JOY) commented Sep 26, 2026 •

Copy link
Copy Markdown

Upstream sync 4c33d52..374fb20 (42 commits since upstream-sync-20260922 / PR #59).

Features: inline preview comments with replies, resolve and anonymous commenting (optional reCAPTCHA v2 invisible), connected clients, schedule post preview url.

Fixes: Instagram container error curation (2207082/2207085 + disconnect mapping), Threads UNKNOWN container message, Telegram 400 -> non-retryable BadBody, media upload cancel-on-error + removed-file guard, calendar deleted post group, Stripe.js load failure message (new stripeFailed branch in first.billing), extension chrome guards, Facebook reconnect without page token, nostr hex key signing, auth activate redirect race.

Conflict resolutions:

  • .env.example: union - Crove telemetry block kept, upstream reCAPTCHA + provider visibility docs added, REDDIT_CLIENT_ID/SECRET stay removed.
  • first.billing.component: DOS checkout branch (shared billing) preserved AND upstream stripeFailed branch added; Stripe.js only loads outside shared dos billing.
  • variable.context: keep brandConfig, add recaptchaSiteKey.

Schema note (deploy gating): Comments model gains threading/anchor/resolved columns + nullable userId. Beta DB gets the idempotent ALTERs at deploy time; the SHARED PROD DB (gulptwduchsjcsbndmua, schema post) must receive this through the DOS-Me migration ledger BEFORE the prod deploy (rule: no out-of-band DDL on shared prod). Prod deploy is intentionally held until then - a migration file follows via the DOS-Me handoff channel.


📌 TL;DR

Introduces an interactive inline and threaded commenting experience on public post preview pages with anonymous reviewer support, reCAPTCHA v2 protection, and thread resolution, alongside various frontend resilience fixes (Stripe error handling, extension messaging safety, and uppy upload cancellation).


🎯 Type of Change

  • 🚀 New feature
  • 🐛 Bugfix
  • 🧹 Refactor
  • ⚡ Performance
  • 📚 Documentation
  • ⚙️ CI / Configuration

🔍 Changes Walkthrough

File Summary of Changes
.env.example Added environment variable documentation for reCAPTCHA v2 keys and provider migration/visibility settings.
.zcodeignore Added ignore rules for local IDE/tooling artifacts.
apps/backend/src/api/routes/posts.controller.ts Updated POST /posts/:id/comments to use CreatePublicCommentDto and capture client IP; added PUT /posts/comments/:commentId/resolve endpoint for resolving comment threads.
apps/backend/src/api/routes/public.controller.ts Added POST /public/posts/:id/comments endpoint allowing anonymous comments with IP tracking.
apps/frontend/src/app/(app)/(preview)/p/[id]/layout.tsx Updated background and text color tokens for preview layouts.
apps/frontend/src/app/(app)/(preview)/p/[id]/page.tsx Wrapped preview page in PreviewCommentsProvider, adopted modern design system tokens, and replaced static post text with PostContentClient to support anchored inline selection commenting.
apps/frontend/src/app/(app)/layout.tsx Passed recaptchaSiteKey down to client-side app variables context.
apps/frontend/src/app/global.scss Added custom inline-commenting cursor style .preview-comment-cursor.
apps/frontend/src/components/admin/admin-stats.component.tsx Added summary card and platform breakdown table for connectedClients metrics.
apps/frontend/src/components/auth/after.activate.tsx Handled onboarding response header before parsing JSON to prevent crashes during onboarding activation redirects.
apps/frontend/src/components/billing/first.billing.component.tsx Added graceful fallback messaging when Stripe fails to load due to ad-blockers or privacy extensions.
apps/frontend/src/components/launches/add.provider.component.tsx
continue.integration.tsx
menu/menu.tsx
Added runtime checks for Chrome extension presence and handled chrome.runtime.lastError callbacks to prevent unhandled errors.
apps/frontend/src/components/launches/calendar.tsx Added guard toast and early exit when opening a post group that returns no posts.
apps/frontend/src/components/media/new.uploader.tsx Switched uppy.clear() to cancelAll() on error and added null checks for current file state in upload-success.
apps/frontend/src/components/preview/comments.components.tsx Overhauled preview comment component to support anonymous reviewer naming modal, invisible reCAPTCHA v2 execution, threaded replies, inline text anchor quotes, and thread resolution toggles.

📊 Architectural Flow

sequenceDiagram
    autonumber
    actor Reviewer as Reviewer (Anonymous / Logged In)
    participant UI as Preview
<!-- /dos-pr-summary -->
Loading

…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.
…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.
return;
}
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
setFlash(true);
Comment on lines +354 to +367
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]);
Comment on lines +354 to +367
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]);
Comment on lines +354 to +367
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]);
Comment on lines +354 to +367
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]);
Comment on lines +44 to +46
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]);

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust preview commenting system, enabling users to leave general comments, replies, and text-anchored inline comments on posts. It includes backend schema and controller updates, reCAPTCHA v2 integration for anonymous comments, rate-limiting, and frontend components for text selection and highlight rendering. Additionally, it addresses various social provider enhancements and bug fixes. The code review identified several critical issues: detachStaleAnchors is incorrectly called with old post data instead of updated content, preventing stale anchors from being detached; retryable errors in InstagramProvider are incorrectly treated as terminal exceptions; onDone is missing from a useCallback dependency array; unstable array references in PostContentClient cause redundant DOM mutations; unsafe non-null assertions are used on potentially null values; and the reCAPTCHA verification network request lacks error handling for network failures.

Comment on lines +980 to +983
const existingIds = (post.value || []).map((p) => p.id).filter(Boolean);
await this.detachStaleAnchors(
posts.filter((p) => existingIds.includes(p.id))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In posts.service.ts, detachStaleAnchors is called with posts.filter(...) where posts contains the old post data loaded from the database before the update. This means the anchor quotes are compared against the old content, so they will always match and never be identified as stale or detached. To correctly detach stale anchors when a post is updated, you should pass the new content from post.value instead.

Suggested change
const existingIds = (post.value || []).map((p) => p.id).filter(Boolean);
await this.detachStaleAnchors(
posts.filter((p) => existingIds.includes(p.id))
);
const existingIds = (post.value || []).map((p) => p.id).filter(Boolean);
await this.detachStaleAnchors(
(post.value || []).filter((p) => p.id) as any
);

Comment on lines 655 to 671
if (status_code === 'ERROR' || status_code === 'EXPIRED') {
const handleError = this.handleErrors(status || '', 200);
if (handleError?.type === 'disconnect') {
throw new Disconnect(
this.identifier,
JSON.stringify({ status_code, status }),
'{}',
handleError?.value
);
}
throw new BadBody(
this.identifier,
JSON.stringify({ status_code, status }),
'{}',
status || 'Instagram could not process the media'
handleError?.value || status || 'Instagram could not process the media'
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In InstagramProvider.check, if the container status is ERROR or EXPIRED, handleErrors is called to check for specific error codes. If handleErrors returns a retry type (such as for error code 2207082), the code still throws a BadBody exception, which is a terminal, non-retryable error. This defeats the purpose of identifying retryable errors. You should handle the retry type by throwing a standard Error or another retry-signaling exception so the queue worker can retry the job.

    if (status_code === 'ERROR' || status_code === 'EXPIRED') {
      const handleError = this.handleErrors(status || '', 200);
      if (handleError?.type === 'disconnect') {
        throw new Disconnect(
          this.identifier,
          JSON.stringify({ status_code, status }),
          '{}',
          handleError?.value
        );
      }
      if (handleError?.type === 'retry') {
        throw new Error(handleError.value || 'Instagram temporary processing error');
      }
      throw new BadBody(
        this.identifier,
        JSON.stringify({ status_code, status }),
        '{}',
        handleError?.value || status || 'Instagram could not process the media'
      );
    }

setLoading(false);
}
},
[user?.id, recaptchaSiteKey, previewId, parentId, anchor, askForName]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The submit callback in CommentComposer does not include onDone in its dependency array. Since onDone is a prop that can change, this can lead to a stale closure where an outdated version of onDone is called. Please add onDone to the dependency array of useCallback.

Suggested change
[user?.id, recaptchaSiteKey, previewId, parentId, anchor, askForName]
[user?.id, recaptchaSiteKey, previewId, parentId, anchor, askForName, onDone]

Comment on lines +161 to +174
const ranges = useMemo<HighlightRange[]>(
() =>
comments
.filter(
(c) =>
c.postId === postId &&
!c.parentId &&
!c.resolvedAt &&
c.anchorStart !== null &&
c.anchorEnd !== null
)
.map((c) => ({ id: c.id, start: c.anchorStart!, end: c.anchorEnd! })),
[comments, postId]
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ranges array is recreated on every change to comments because .map returns a new array reference. This causes the useEffect that calls applyHighlights to run on every comment update (including replies or unrelated comment resolutions), leading to unnecessary DOM mutations and potential performance issues. You can optimize this by using a stringified representation of ranges as the dependency for the useEffect.

Suggested change
const ranges = useMemo<HighlightRange[]>(
() =>
comments
.filter(
(c) =>
c.postId === postId &&
!c.parentId &&
!c.resolvedAt &&
c.anchorStart !== null &&
c.anchorEnd !== null
)
.map((c) => ({ id: c.id, start: c.anchorStart!, end: c.anchorEnd! })),
[comments, postId]
);
const ranges = useMemo<HighlightRange[]>(
() =>
comments
.filter(
(c) =>
c.postId === postId &&
!c.parentId &&
!c.resolvedAt &&
c.anchorStart !== null &&
c.anchorEnd !== null
)
.map((c) => ({ id: c.id, start: c.anchorStart!, end: c.anchorEnd! })),
[comments, postId]
);
const rangesStr = JSON.stringify(ranges);

Comment on lines +185 to +197
ref.current
?.querySelectorAll<HTMLElement>('mark[data-thread-ids]')
.forEach((mark) => {
const ids = mark.dataset.threadIds!.split(',');
mark.classList.toggle(
'ring-1',
ids.includes(hoveredThread!) || ids.includes(activeThread?.id!)
);
mark.classList.toggle(
'ring-btnPrimary',
ids.includes(hoveredThread!) || ids.includes(activeThread?.id!)
);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using non-null assertions (!) on hoveredThread and activeThread?.id inside ids.includes(...) is unsafe because these values can be null or undefined. It is safer and cleaner to explicitly check if they are defined before calling includes.

    ref.current
      ?.querySelectorAll<HTMLElement>('mark[data-thread-ids]')
      .forEach((mark) => {
        const ids = mark.dataset.threadIds!.split(',');
        const isHovered = hoveredThread ? ids.includes(hoveredThread) : false;
        const isActive = activeThread?.id ? ids.includes(activeThread.id) : false;
        mark.classList.toggle('ring-1', isHovered || isActive);
        mark.classList.toggle('ring-btnPrimary', isHovered || isActive);
      });

Comment on lines +1476 to +1486
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fetch call to Google's reCAPTCHA API is not wrapped in a try-catch block. If there is a network error or DNS failure, the promise rejection will bubble up as an unhandled 500 Internal Server Error. It is safer to wrap the network request in a try-catch block and throw a friendly BadRequestException.

    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');
    }

@JOY

Copy link
Copy Markdown
Author

ESLint code-scanning check (10 new alerts incl. 6 errors): same accepted class as the documented baseline - react-compiler style rules (exhaustive-deps, preserve-manual-memoization, set-state-in-effect, immutability) + no-img-element, overwhelmingly in upstream's new preview-comments/auth files. The repo's own eslint.yml gate passes on this PR; the merge-introduced files (first.billing, variable.context, .env.example) lint clean at 0 errors. Accepted per the documented precedent from PR #40/#59 (upstream ESLint findings accepted, refactor/suppress as separate follow-up). Upstream code is not refactored here per the standing no-shared-UI-rebuilds directive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants