feat(activity): Activity feed view (mentions, threads, reactions, DMs) - #109
genki-miyachi wants to merge 6 commits into
Conversation
|
hi @genki-miyachi thanks for this! I love the idea of having the activity feed in the app. one note when testing, it looks like maybe we're not showing all the data. if I compare it to the actual activity feed, there's a lot of data about what the activity was, e.g. an emoji added, a reply, etc. |
Addresses the review note on gammons#109 that the feed showed only metadata, not "what the activity was". activity.feed returns refs only; each loaded page now triggers a second messages.list fetch to hydrate the message bodies. Rows are now two-line cards mirroring the desktop Activity feed: <author> Mention/Thread/Reacted in #channel <time> [●] <message body preview> - client: GetActivityMessages(refs) via messages.list, keyed by ActivityMsgKey(channel, ts); parsed leniently per channel. - Hydrate wired through ActivityService; ActivityListLoadedMsg kicks the body fetch, ActivityBodiesLoadedMsg (team-guarded) installs them. - view: author-first cards with a natural-language context label, the hydrated body on line 2 (empty until hydration lands), reaction emoji prefixed; author falls back to the hydrated message's user for thread_v2/dm refs that carry no author on the feed item. - Extract slack markup stripping into internal/slackfmt.StripMarkup (no length cap; callers clip to width) so the view doesn't depend on the notification formatter; notify.StripSlackMarkup now delegates and caps by rune (fixing a latent mid-rune byte-truncation of multibyte bodies). Relates to gammons#109. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… switch - Clear hydrated bodies on workspace switch/reset (SetBodies(nil) alongside the existing SetItems(nil)) so team A's bodies can't linger under team B. - Tests: parseActivityMessages (fixture, lenient — empty-ts skipped, malformed channel skipped, ok:false wrapped), ActivityMsgKey no-collision, activityRefs dedup/skip-empty/grouping, slackfmt.StripMarkup (resolves + strips, no truncation, UTF-8 safe on multibyte), and the activity view's card invariants: renderCard always exactly cardStride lines with no embedded newline (incl. newline/empty/CJK/absent-body bodies), renderRows stride, contextVerb/contextLabel, author fallback to hydrated body, and ClickAt mapping both card lines to one item. Relates to gammons#109. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relates to gammons#109. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… DMs) Adds a Slack-native "Activity" view -- the desktop client's Activity tab -- as a dedicated list in the message pane, mirroring the existing Threads view. Backed by Slack's internal activity.feed endpoint, reached with the same xoxc browser auth slk already uses for client.counts, subscriptions.thread.getView, etc. Surfaces what notified you: @mentions (at_user/at_channel/at_everyone/ at_user_group/keyword), thread replies (thread_v2), reactions to your messages (message_reaction), and DMs (dm/bot_dm_bundle). The endpoint's server-side unread_only flag drives an in-view unread/read filter. Entry points: an Activity sidebar row (below Threads), a ctrl+a toggle (the terminal stand-in for the desktop's Cmd+Shift+A), and an :activity command. `u` toggles the unread-only filter; Enter opens the underlying message/thread in its channel. Parsing is per-item and lenient -- a single item with an unexpected field type is skipped rather than failing the page -- and a failed fetch keeps the existing feed instead of blanking it. Relates to gammons#12. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the review note on gammons#109 that the feed showed only metadata, not "what the activity was". activity.feed returns refs only; each loaded page now triggers a second messages.list fetch to hydrate the message bodies. Rows are now two-line cards mirroring the desktop Activity feed: <author> Mention/Thread/Reacted in #channel <time> [●] <message body preview> - client: GetActivityMessages(refs) via messages.list, keyed by ActivityMsgKey(channel, ts); parsed leniently per channel. - Hydrate wired through ActivityService; ActivityListLoadedMsg kicks the body fetch, ActivityBodiesLoadedMsg (team-guarded) installs them. - view: author-first cards with a natural-language context label, the hydrated body on line 2 (empty until hydration lands), reaction emoji prefixed; author falls back to the hydrated message's user for thread_v2/dm refs that carry no author on the feed item. - Extract slack markup stripping into internal/slackfmt.StripMarkup (no length cap; callers clip to width) so the view doesn't depend on the notification formatter; notify.StripSlackMarkup now delegates and caps by rune (fixing a latent mid-rune byte-truncation of multibyte bodies). Relates to gammons#109. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… switch - Clear hydrated bodies on workspace switch/reset (SetBodies(nil) alongside the existing SetItems(nil)) so team A's bodies can't linger under team B. - Tests: parseActivityMessages (fixture, lenient — empty-ts skipped, malformed channel skipped, ok:false wrapped), ActivityMsgKey no-collision, activityRefs dedup/skip-empty/grouping, slackfmt.StripMarkup (resolves + strips, no truncation, UTF-8 safe on multibyte), and the activity view's card invariants: renderCard always exactly cardStride lines with no embedded newline (incl. newline/empty/CJK/absent-body bodies), renderRows stride, contextVerb/contextLabel, author fallback to hydrated body, and ClickAt mapping both card lines to one item. Relates to gammons#109. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relates to gammons#109. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guards the resolution behavior moved into slackfmt when reconciling the Activity hydration with gammons#96's notify usergroup support: bare <!subteam^ID> resolves via the usergroup map, labeled forms use the embedded label, and an unresolved bare token falls back to "@group".
f956646 to
fe851f5
Compare
|
@gammons
Also rebased onto main to resolve the conflict with the recent notify/usergroups changes — the markup stripping now lives in |
|
hi @genki-miyachi thanks for that. there are a couple of merge conflicts now. but I think this is ready to get merged in. |
# Conflicts: # internal/ui/app.go # internal/ui/keys.go # internal/ui/reducer_channels.go
|
@gammons Conflicts resolved — merged latest Only real overlap was the keymap: #110's
|
|
Hi +1 for this feature, it's the one blocker for me to be able to use this package. I spend 99% of my time in the activity tab and no tool has good keyboard shortcuts so slk will solve a ton of problems for me! Any progress / blockers on getitng this merged? I'm happy to help if needed |
|
@genki-miyachi — apologies, this went stale on my end, not yours. You did two rounds of requested revisions and I told you it was ready, then let it sit. That's on me. I've now reviewed this side by side with #169, which implements the same feature, and this is the one I'm merging. The deciding factor is that this hydrates message bodies via A few other things I checked and liked:
Four things and then I merge:
Two follow-ups I'd like but won't hold this for — @agustif got both right in #169 and I'll credit them there:
Thanks for your patience with this one. Ping me when it's rebased. Closes #12. |


What
Adds a Slack-native Activity view — the desktop client's 🔔 Activity tab — as a dedicated list in the message pane, mirroring the existing Threads view (
⚑ Threads). It's the "catch up on what pinged me" view a few people asked for in #12.How
Sourced from Slack's internal
activity.feedendpoint, reached with the samexoxcbrowser auth slk already uses forclient.counts,subscriptions.thread.getView, etc. (viapostForm). It surfaces exactly what the desktop Activity tab does:at_user/at_channel/at_everyone/at_user_group/keywordthread_v2message_reactiondm/bot_dm_bundleactivity.feedtakes a server-sideunread_onlyflag, so the read/unread filter is done by Slack rather than client-side.UX
🔔 Activitysidebar row (below⚑ Threads), actrl+atoggle (the terminal-friendly stand-in for the desktop's ⌘⇧A), and an:activitycommand.utoggles the unread-only filter (re-fetches).Enteropens the underlying message/thread in its channel, reusing the existing channel-open / permalink-jump / thread-open paths.feed_ts; unread items are bold.Design notes
ViewActivityalongsideViewThreads: view enum, app field/init, msgs, a reducer, the render branch, nav/scroll, the sidebar synthetic row).ActivityItem{Key,Type,IsUnread,ChannelID,TS,ThreadTS,AuthorID,Reaction,…}so the UI never touches raw JSON.messages.list) andactivity.markReadare intentionally left as follow-ups; opening an item already marks the underlying conversation read.Testing
internal/slack/activity_test.goparses a sanitized realactivity.feedresponse fixture (all item types) and asserts per-type extraction, plus a regression test that one malformed item is skipped without failing the page.go build ./...,go vet ./...,go test ./...all pass.ufilter →Enterto open). Easy to try:gh pr checkout, build, run against your own workspace.Happy to rebind any keys (
ctrl+a/u) — those were just what was free. Relates to #12.(Full disclosure: built with heavy AI assistance — frontier model, high effort — and reviewed + tested on my end, per the contributing notes.)
🤖 Generated with Claude Code