Repository navigation
Conversation
Maintainers can pin up to 25 messages in an org. Everyone in the org sees pinned messages, including people added without the history, and can reply and react to them. A one-line banner at the top of the thread opens a panel with each pinned message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Org maintainers can now pin messages. Everyone in the org sees pinned messages, including people who were added without Share history and can't read anything from before they joined.
Pinning sits next to Reply and React on a message. Each time a maintainer pins, a dialog warns them that the whole org will see the message, history or not, until they tick "Don't show this again". A pinned message keeps its pin button visible beside it, so maintainers can unpin it without hovering, and contributors see a plain pin icon there.
Pinned messages show as a one-line bar at the top of the chat: a count and the latest pin. Clicking it opens a panel with each pinned message, its author and time, the text cut to three lines with Show more, and Reply and React. Maintainers can unpin from the panel too.
Changes
messages.pinned_atis a new nullable timestamp with an index on(conversation_id, pinned_at). It is needed to know which messages an org pinned and in what order. Everyone in the org can see it. It is cleared on unpin and goes with the message.UpdateMessagePinandDeleteMessagePinset and clear it throughMessagePinController(PUT/DELETE remotes/messages/{message}/pin). The form requests allow maintainers of the message's org only, not on events, not on messages older than the maintainer's own history, and at most 25 pins per org (Conversation::MAX_PINS). Pinning is blocked during a timeout like the other org actions.Message::isVisibleTo()andRepliableMessagetreat a pinned message as visible to every member of its org. That is what lets someone without history load its images and voice note, react, and reply. Unpinning takes that access away again.pinnedlist. To avoid a new column, pinning and unpinning move the conversation's existingreactions_version, so polls only get the list again when a pin or a reaction changed (pinned: nullotherwise). The pin endpoints return the list and that version, and the client only accepts a list at least as new as the one it has, so a slow poll can't undo a pin.reply_to,reply_to_idand link previews. Otherwise a member without history could read what an old message replied to.ReactionTraywith a newplacement="beside", so the emoji tray opens next to the item's buttons.pinnedtoo, throughMessageResource. There are no pin endpoints in the API yet, so the knowledge base says pins show on the web.Things to look at:
reactions_version, so the next poll for each member re-sends the pinned list (up to 25 messages). That felt like a fair trade for not adding a column, but say if you'd rather have a separate version.Validation
Local, in a clone on this branch:
pint --parallel --test,rector --dry-runandphpstanpass.pest --type-coverage --min=100is at 100%.pest --parallel --ci --coverage --exactly=100.0passes, 3987 tests, 100% coverage.tscruns and the node unit tests (147) pass, as does the build. These ran throughnode_modules/.binbecause bun isn't installed on this machine.New tests cover who can pin and unpin, the cap, timeouts, events, history limits, the
pinnedlist on loads and polls, a member without history seeing, reacting to, replying to and loading the image of a pinned message, and the pinned list not leaking what it replies to. The client's pin bookkeeping has unit tests intests/Unit/Pins.test.ts.I also used it in the browser as the maintainer and as a member added without history. That covered pinning, the warning, the banner and panel, Show more, reacting and replying from the panel, and unpinning.
Layout shift, measured with a
layout-shiftobserver:Visual evidence
All captures are from a local copy with seeded data, in Chrome through Playwright. The before shots come from
productionat the base commit with the same data.The two failures were faked with Playwright's
page.route, so no request reached the server.Device verification
No physical device. Touch was checked in Chrome's device emulation at 390x844 (
isMobile,hasTouch, real touch events through CDP), so it says nothing about the on-screen keyboard or safe areas on a real phone.Independent review
A separate agent that didn't write the code reviewed it independently, read-only, on base
8adba040and head3fdfdaa0, against the original request, the repo guidelines and every capture above. It took three rounds.3fdfdaa0: CODE: PASS, VISUAL: PASS, no blockers. It confirmed each earlier finding is fixed.Limitations
🤖 Generated with Claude Code