Skip to content

feat: add ally system, user profile popover, and reaction tooltips#22

Merged
BuckyMcYolo merged 6 commits intomainfrom
dev
Mar 22, 2026
Merged

feat: add ally system, user profile popover, and reaction tooltips#22
BuckyMcYolo merged 6 commits intomainfrom
dev

Conversation

@BuckyMcYolo
Copy link
Copy Markdown
Owner

@BuckyMcYolo BuckyMcYolo commented Mar 21, 2026

  • Ally (friend) system: schema, API endpoints (send/accept/decline requests, list allies, remove), and allies page UI with tabs
  • User profile popover on avatar/name click in messages and member list showing bio, status, presence, and ally actions
  • Reaction tooltips showing who reacted (with "You" for current user) including real-time reactor list updates

Pull Request Summary: Ally System, User Profile Popover, Reaction Tooltips, and DM improvements

Overview

This PR implements an ally (friend) system (DB schema + API + UI), a user profile popover with ally actions, reaction tooltips that show who reacted, and DM creation UI/APIs. Changes span database schemas, API routes, realtime events, frontend components, hooks, and types.

Backend Changes

Database Schema

  • Added ally_request Drizzle schema (packages/db/src/schemas/ally-requests.ts):
    • Enum ally_request_status (pending, accepted, declined)
    • Table ally_request (id, createdAt, updatedAt, senderId → user.id, receiverId → user.id, status)
    • Unique index on (senderId, receiverId) and additional indexes
    • Select/insert Zod schemas and relations to user

Note about migrations: generated Drizzle migration artifacts are not present in the repo. The PR author explicitly requested that migration files not be committed (the project uses db:push locally / CI), and an automated bot in the thread acknowledged that policy.

Allies API Routes (/v1/allies)

New route tree providing:

  • POST /allies/requests — send ally request (prevents self-requests, handles existing requests, transactional re-create on declined)
  • GET /allies/requests — list pending incoming/outgoing requests
  • POST /allies/requests/{requestId}/accept — accept (receiver-only; enforces pending status)
  • POST /allies/requests/{requestId}/decline — decline (receiver-only)
  • GET /allies — list accepted allies (bidirectional)
  • DELETE /allies/{userId} — remove ally relationship

All routes use sessionAuthMiddleware and Zod/OpenAPI schemas with appropriate response mappings.

Users API

  • GET /users/{userId} — returns user profile with presenceStatus (via Redis membership check, fail-open to offline if Redis access fails), allyStatus (none, pending_incoming, pending_outgoing, allies), and allyRequestId when applicable.

DMs API

  • POST /dms — create DM/group DM with deduped userIds; requires accepted ally relationships with targets (responds 403 if any target is not an ally); returns existing 1:1 DM if present; transactional creation for new DM/group DM.

Reaction & Realtime

  • Message reaction schemas and DB queries extended with reactors: Array<{ id, name }>
  • fetchMessagePage and listPinnedMessages join user table to gather reactor names and aggregate reactors per emoji
  • Realtime event payloads include actorName and reactors; realtime service emits actorName on toggle events
  • Realtime types updated accordingly

Frontend Changes

UI Components

  • AlliesPage: Allies management UI (All / Pending tabs), search, send request form, accept/decline, remove ally, start DM action, confirmation dialogs, query invalidation, and toast feedback
  • UserProfilePopover / ProfileCardContent: popover that fetches profile, shows presence/bio/status/member-since, and renders AllyActionButton adapting to allyStatus with send/accept/remove flows
  • NewDMDialog + DMPanel updates: dialog to select allies and create DM/group DM; DM panel wired to open dialog; DMs index now renders AlliesPage
  • MessageItem & Guild Members: avatar/name wrapped in UserProfilePopover triggers; reactions wrapped in tooltips showing reactor names (current user labeled "You")
  • useCreateDM hook: posts to create DM and navigates to the DM on success

Hooks & Client Logic

  • useMessageReactions accepts optional currentUserName and passes current user info into optimistic toggles
  • realtime-adapter maintains per-reaction reactors arrays; toggleReactionOptimistically updated to accept optional currentUser
  • ChannelView and DMConversation pass session?.user.name into message reaction hook
  • API client types extended for allies/users responses

Notable Files Touched

  • apps/api/src/app.ts — mounts allies and users routers under /v1
  • packages/db/src/schemas/ally-requests.ts — new schema
  • apps/api/src/routes/v1/allies/* — routes, handlers, schemas added
  • apps/api/src/routes/v1/users/* — routes, handler, schemas added
  • apps/api/src/routes/v1/dms/* — createDM handler/routes/schemas added
  • apps/realtime and packages/realtime-types — event/payload updates
  • apps/web/src/components/... — AlliesPage, UserProfilePopover, NewDMDialog, message-item, dm-panel changes
  • apps/web/src/hooks and lib — reaction hooks and realtime-adapter updates
  • apps/web/src/lib/api-types.ts — new allies/users types

Issues & Recommendations

  1. Migrations / Deployment

    • The Drizzle schema is added but generated migration artifacts are not committed. Per the author’s instruction and bot acknowledgement, this repo uses db:push (db:push should be run in developer/CI environments). Ensure CI/CD or deployment runs drizzle-kit push (or otherwise applies schema changes) before services start, or adjust team policy if committed migrations are required.
  2. Tests / Coverage

    • No tests were added for ally flows, DM creation (including non-ally permission checks), presence fallback behavior, or reaction tooltip logic. Adding unit/integration tests for these critical flows is recommended.
  3. Concurrency & Edge Cases

    • Transactional logic and conditional updates exist for ally requests and DM creation. Review behavior under high concurrency (unique constraint violations, retry strategies) and consider adding explicit conflict-handling/retry patterns where appropriate.
  4. UX Change Confirmation

    • The DMs index route now renders AlliesPage (replacing prior DMs home). Confirm this UX change is intentional.

Positive Notes

  • End-to-end coverage across DB, API, realtime, and frontend is cohesive and consistent.
  • Zod/OpenAPI usage provides clear request/response contracts.
  • Optimistic UI updates and realtime reactor-name propagation improve user experience.
  • Presence check uses a sensible fail-open approach to avoid hard failures when Redis is unavailable.
  • DM creation reuses existing 1:1 channels and uses transactions to mitigate duplicates.

Confidence Score: 4/5

Rationale: Implementation is well-structured and consistent across stacks, with clear schemas and OpenAPI contracts. The only significant operational caveat is the absence of committed migration artifacts — which appears to be an intentional repo policy (db:push) per author/bot discussion — plus a lack of tests. If CI/CD applies schema changes and tests are added, this PR is production-ready with minor nits only.

- Ally (friend) system: schema, API endpoints (send/accept/decline
  requests, list allies, remove), and allies page UI with tabs
- User profile popover on avatar/name click in messages and member
  list showing bio, status, presence, and ally actions
- Reaction tooltips showing who reacted (with "You" for current user)
  including real-time reactor list updates
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an allies/friend system (DB schema, API routes, handlers, OpenAPI schemas), user profile endpoint with presence and ally status, DM creation with ally checks and reuse, reaction reactor tracking end-to-end (types → queries → realtime → web), UI for allies/DM creation and user-profile popovers, and mounts new v1 routers into the app.

Changes

Cohort / File(s) Summary
Allies API & Routes
apps/api/src/routes/v1/allies/routes.ts, apps/api/src/routes/v1/allies/schema.ts, apps/api/src/routes/v1/allies/handlers.ts, apps/api/src/routes/v1/allies/index.ts
Adds /v1/allies router with six endpoints (send/list/accept/decline/list/remove), Zod/OpenAPI schemas, and full handler implementations (validation, transactions, bulk user fetches).
Allies DB Schema & Exports
packages/db/src/schemas/ally-requests.ts, packages/db/src/schemas/index.ts
Introduces ally_request table and ally_request_status enum, indexes, relations, select/insert Zod schemas, and re-exports the module.
User Profile API
apps/api/src/routes/v1/users/routes.ts, apps/api/src/routes/v1/users/schema.ts, apps/api/src/routes/v1/users/handlers.ts, apps/api/src/routes/v1/users/index.ts
Adds GET /v1/users/{userId} with Redis-backed presenceStatus, allyStatus/allyRequestId resolution, OpenAPI/Zod schemas and handler.
DM API
apps/api/src/routes/v1/dms/routes.ts, apps/api/src/routes/v1/dms/handlers.ts, apps/api/src/routes/v1/dms/schema.ts, apps/api/src/routes/v1/dms/index.ts
Adds createDM route/handler/schema with input validation, ally-based authorization, 1:1 DM reuse detection, transactional creation, and mapped BAD_REQUEST/FORBIDDEN responses.
App Routing & OpenAPI
apps/api/src/app.ts, apps/api/src/lib/helpers/openapi/schemas.ts
Mounts alliesRouter and usersRouter under /v1 (affecting exported AppType) and adds badRequestSchema OpenAPI JSON content constant.
Message Reaction Types & Queries
packages/realtime-types/src/events.ts, apps/api/src/lib/helpers/openapi/message-schemas.ts, apps/api/src/lib/queries/messages.ts, apps/api/src/routes/v1/channels/handlers.ts
Extends reaction types/schemas with reactors and actorName; queries and handlers join user names and aggregate per-reaction reactors.
Realtime Service Changes
apps/realtime/src/index.ts, apps/realtime/src/services/messages.ts
Socket handler forwards userName; toggleMessageReaction input type extended with userName and returns actorName in update payload.
Web: Allies UI & Popover
apps/web/src/components/allies/allies-page.tsx, apps/web/src/components/ui/user-profile-card.tsx, apps/web/src/lib/api-types.ts
Adds AlliesPage, UserProfilePopover/profile card, ally-related mutations, query invalidation, and API type aliases for allies and user profiles.
Web: Realtime Reaction Handling
apps/web/src/lib/realtime-adapter.ts, apps/web/src/hooks/use-message-reactions.ts
Optimistic toggle and realtime application now maintain per-reaction reactors; hook accepts optional currentUserName to populate reactors during optimistic updates.
Web: UI Integrations & DM flows
apps/web/src/components/chat/message-item.tsx, apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx, apps/web/src/components/sidebar/dm-panel/..., apps/web/src/routes/_authenticated/...
Wraps avatars/names with UserProfilePopover, adds reaction tooltips showing reactor names, wires currentUserName into hooks, adds NewDMDialog and DM creation flow, and replaces DMs index with AlliesPage.
Web: New DM UI & Hook
apps/web/src/components/sidebar/dm-panel/new-dm-dialog.tsx, apps/web/src/components/sidebar/dm-panel/dm-panel.tsx, apps/web/src/hooks/use-create-dm.ts
Adds NewDMDialog, toggles DM panel button to open dialog, and adds useCreateDM hook to create DM and navigate on success.
LocalStorage & Routing UX
apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx, apps/web/src/routes/_authenticated/$guildSlug/index.tsx
Persists last-viewed channel per guild to localStorage and restores it on guild home; passes currentUserName into message-reaction hooks.

Sequence Diagram

sequenceDiagram
    participant Client as Client (web)
    participant Realtime as Realtime Service
    participant API as API Server
    participant DB as Database
    participant Redis as Redis

    Client->>Realtime: emit "message:reaction:toggle" (messageId, emoji, userId, userName)
    Realtime->>API: call toggleMessageReaction(input including userName)
    API->>DB: query/update reactions, join user for reactor names
    DB-->>API: reaction rows (including reactor user names)
    API->>Realtime: update payload (emoji, count, actorUserId, actorName, reactors)
    Realtime-->>Client: broadcast "message:reaction:updated" (payload)
    Client->>Client: applyReactionUpdateToMessage (updates reactors array and UI)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped into code with a twitch and a twirl,
Friends can send requests and reaction flags whirl.
Names beside emojis, popovers that gleam,
Presence, DMs, and allies — a rabbit's bright dream.
🥕 I left a tiny patch and a crunchy carrot beam.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main changes: it introduces the ally system, user profile popover, and reaction tooltips across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/routes/v1/channels/handlers.ts (1)

421-441: 🧹 Nitpick | 🔵 Trivial

Consider extracting shared reaction aggregation logic.

The reaction aggregation logic (type definition, Map initialization, and population) is duplicated between listPinnedMessages and fetchMessagePage in apps/api/src/lib/queries/messages.ts. Consider extracting a shared helper to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/channels/handlers.ts` around lines 421 - 441, The
reaction aggregation logic (building reactionsByMessageId Map from reactionRows)
is duplicated in listPinnedMessages and fetchMessagePage; extract this into a
shared helper function (e.g., buildReactionsByMessageId or aggregateReactions)
that accepts reactionRows and returns Map<string, Map<string,
{emoji:string,count:number,reactedByCurrentUser:boolean,reactors:Array<{id:string,name:string}>}>>;
replace the duplicated blocks in listPinnedMessages and fetchMessagePage to call
this helper and ensure the helper handles Map initialization, incrementing
count, and pushing reactors for each row (using the same row fields: messageId,
emoji, userId, userName).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app.ts`:
- Around line 40-46: The API has new route mounts in app.ts (.route("/v1",
alliesRouter), channelsRouter, guildsRouter, invitesRouter, dmsRouter,
uploadsRouter, usersRouter) but the autogenerated frontend API client was not
rebuilt; regenerate the API client artifacts so the frontend/type definitions
reflect these new routes. Run the project’s API client generation/build script
(e.g., the repo’s api-client build or generate command) after pulling the
changes so the updated OpenAPI/TS types are emitted, then commit the regenerated
client artifacts.

In `@apps/api/src/routes/v1/allies/handlers.ts`:
- Around line 252-257: When receiverMap.get(row.receiverId) returns undefined
you currently silently continue; update the handler to log a warning before the
continue so missing receivers are visible in logs: emit a warn including
row.receiverId (and row.id or other identifying fields) plus currentUser.id to
provide context. Use the module's existing logger (e.g., logger or
processLogger) and keep the current flow (log then continue); touch the receiver
lookup block referencing receiverMap, row.receiverId, currentUser and
currentUserInfo.
- Around line 133-137: The delete-then-insert sequence for replacing a declined
request (using db.delete(schema.allyRequest).where(eq(schema.allyRequest.id,
existing.id)) followed by the subsequent db.insert(schema.allyRequest) call) is
vulnerable to a race that can produce duplicate/unique-constraint errors; make
the operation atomic by wrapping the delete+insert in a single transaction or by
switching to an upsert pattern (e.g., transactional db.transaction(...) that
performs the delete and insert within the same TX or using
db.insert(...).onConflict(...).merge()/upsert equivalent) so the replacement
cannot be interleaved by concurrent requests.

In `@apps/api/src/routes/v1/allies/routes.ts`:
- Around line 24-49: The route sendAllyRequest currently declares a successful
response under HttpStatusCodes.OK; change the route's responses entry to use
HttpStatusCodes.CREATED for the success response (keep using
sendAllyRequestResponseSchema and the existing description), and update the
route handler that sends the response to return HttpStatusCodes.CREATED instead
of HttpStatusCodes.OK so the endpoint correctly signals resource creation.

In `@apps/web/src/components/allies/allies-page.tsx`:
- Around line 349-356: The current code uses removeAlly.isPending which disables
every AllyRow; instead track the specific ally being removed by adding a local
piece of state (e.g., removingAllyId) and set it when initiating
removeAlly.mutate and clear it in the mutation's onSuccess/onError handlers.
Update the AllyRow prop to isRemoving={removingAllyId === ally.id} and change
the onRemove call to first set removingAllyId = ally.id before calling
removeAlly.mutate, then clear removingAllyId in the mutation callbacks (or use
the mutation's lifecycle hooks) so only the targeted row's button is disabled.
- Around line 180-183: The invalidate() helper only clears ["allies"] and
["ally-requests"], leaving cached entries used by UserProfileCard (keyed as
["user-profile", userId]) stale; update invalidate() to also invalidate
user-profile entries (either by invalidating the root ["user-profile"] namespace
or by calling queryClient.invalidateQueries({ queryKey: ["user-profile", userId]
}) when you know the affected userId) and update the mutation callbacks in
allies-page (accept/decline/remove handlers) to pass the relevant userId into
invalidate so the profile popover reflects the new allyStatus immediately.

In `@apps/web/src/components/chat/message-item.tsx`:
- Around line 291-298: In MessageItem
(apps/web/src/components/chat/message-item.tsx) avoid calling .map on a possibly
undefined reaction.reactors by using a defensive fallback (e.g., const reactors
= reaction.reactors ?? []), then derive names from reactors and build
tooltipText from that safe array; update any uses of reaction.reactors.map(...)
to use the fallback variable so hydrated REST messages won't throw at runtime.

In `@apps/web/src/hooks/use-message-reactions.ts`:
- Around line 49-52: The currentUser construction uses truthy checks so an empty
string name yields undefined; change the condition in use-message-reactions.ts
where currentUser is built to use nullish checks (explicit null/undefined) for
currentUserId and currentUserName (e.g., ensure both are not null/undefined via
!= null or !== undefined checks) and only then set currentUser = { id:
currentUserId, name: currentUserName }; otherwise keep it undefined so
blank-but-valid names still preserve optimistic reaction identity.

In `@apps/web/src/lib/realtime-adapter.ts`:
- Around line 148-150: The current spread of currentReaction.reactors can throw
if reactors is undefined; update the reactors assignment in realtime-adapter.ts
so you spread a safe default empty array when reading currentReaction.reactors
(e.g., use a nullish-coalescing or logical-or fallback to an empty array) and
keep the conditional that appends currentUser only when present; target the
expression building the reactors array around currentReaction and currentUser to
implement this safe fallback.
- Around line 138-140: currentReaction.reactors may be undefined when messages
are hydrated from the REST API, causing a TypeError when calling .filter();
update the expression in realtime-adapter.ts to defensively handle undefined by
using a fallback (e.g. (currentReaction.reactors ?? []) ) before filtering so
when currentUser is present use (currentReaction.reactors ?? []).filter(r =>
r.id !== currentUser.id) and when not present use currentReaction.reactors ??
[]; keep references to reactors, currentReaction, and currentUser when making
the change.

---

Outside diff comments:
In `@apps/api/src/routes/v1/channels/handlers.ts`:
- Around line 421-441: The reaction aggregation logic (building
reactionsByMessageId Map from reactionRows) is duplicated in listPinnedMessages
and fetchMessagePage; extract this into a shared helper function (e.g.,
buildReactionsByMessageId or aggregateReactions) that accepts reactionRows and
returns Map<string, Map<string,
{emoji:string,count:number,reactedByCurrentUser:boolean,reactors:Array<{id:string,name:string}>}>>;
replace the duplicated blocks in listPinnedMessages and fetchMessagePage to call
this helper and ensure the helper handles Map initialization, incrementing
count, and pushing reactors for each row (using the same row fields: messageId,
emoji, userId, userName).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0b5481fe-e7c8-42c7-a131-b036bbd59fe5

📥 Commits

Reviewing files that changed from the base of the PR and between d85d061 and 9259153.

📒 Files selected for processing (28)
  • apps/api/src/app.ts
  • apps/api/src/lib/helpers/openapi/message-schemas.ts
  • apps/api/src/lib/helpers/openapi/schemas.ts
  • apps/api/src/lib/queries/messages.ts
  • apps/api/src/routes/v1/allies/handlers.ts
  • apps/api/src/routes/v1/allies/index.ts
  • apps/api/src/routes/v1/allies/routes.ts
  • apps/api/src/routes/v1/allies/schema.ts
  • apps/api/src/routes/v1/channels/handlers.ts
  • apps/api/src/routes/v1/users/handlers.ts
  • apps/api/src/routes/v1/users/index.ts
  • apps/api/src/routes/v1/users/routes.ts
  • apps/api/src/routes/v1/users/schema.ts
  • apps/realtime/src/index.ts
  • apps/realtime/src/services/messages.ts
  • apps/web/src/components/allies/allies-page.tsx
  • apps/web/src/components/chat/message-item.tsx
  • apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx
  • apps/web/src/components/ui/user-profile-card.tsx
  • apps/web/src/hooks/use-message-reactions.ts
  • apps/web/src/lib/api-types.ts
  • apps/web/src/lib/realtime-adapter.ts
  • apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx
  • apps/web/src/routes/_authenticated/dms/$dmId.tsx
  • apps/web/src/routes/_authenticated/dms/index.tsx
  • packages/db/src/schemas/ally-requests.ts
  • packages/db/src/schemas/index.ts
  • packages/realtime-types/src/events.ts

Comment thread apps/api/src/app.ts
Comment thread apps/api/src/routes/v1/allies/handlers.ts Outdated
Comment thread apps/api/src/routes/v1/allies/handlers.ts
Comment thread apps/api/src/routes/v1/allies/routes.ts
Comment thread apps/web/src/components/allies/allies-page.tsx Outdated
Comment thread apps/web/src/components/allies/allies-page.tsx
Comment thread apps/web/src/components/chat/message-item.tsx
Comment thread apps/web/src/hooks/use-message-reactions.ts Outdated
Comment thread apps/web/src/lib/realtime-adapter.ts Outdated
Comment thread apps/web/src/lib/realtime-adapter.ts Outdated
@BuckyMcYolo
Copy link
Copy Markdown
Owner Author

@CodeRabbit we do not commit migration files currently, do not raise an issue for this, we just use db:push locally now

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 21, 2026

Tip

For best results, initiate chat on the files or code changes.

@BuckyMcYolo Understood — and this is already noted in my knowledge base from PR #17. I should not have raised this concern again. I'll make sure migration-related comments are not flagged for this repository going forward. Apologies for the noise!

(ノ◕ヮ◕)ノ*:・゚✧


🧠 Learnings used
Learnt from: BuckyMcYolo
Repo: BuckyMcYolo/townhall PR: 17
File: packages/db/src/schemas/users.ts:36-37
Timestamp: 2026-03-09T16:33:39.652Z
Learning: The `townhall` project (BuckyMcYolo/townhall) uses Drizzle ORM's `db:push` workflow instead of generating migration files. There is no `packages/db/drizzle/` migrations directory; schema changes are applied directly to the database via `db:push`. Do not flag missing migration files for this repository.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
apps/web/src/components/allies/allies-page.tsx (1)

182-190: ⚠️ Potential issue | 🟡 Minor

Profile caches still stay stale after non-remove mutations.

apps/web/src/components/ui/user-profile-card.tsx:81-98 caches profiles under ["user-profile", userId]. This helper can invalidate one of those keys, but only the remove path uses it. Sending, accepting, or declining a request can still leave an already-cached popover on the old allyStatus.

♻️ Minimal fix
-  const invalidate = (affectedUserId?: string) => {
+  const invalidate = () => {
     void queryClient.invalidateQueries({ queryKey: ["allies"] })
     void queryClient.invalidateQueries({ queryKey: ["ally-requests"] })
-    if (affectedUserId) {
-      void queryClient.invalidateQueries({
-        queryKey: ["user-profile", affectedUserId],
-      })
-    }
+    void queryClient.invalidateQueries({ queryKey: ["user-profile"] })
   }
@@
-    onSuccess: (userId) => {
+    onSuccess: () => {
       setRemovingAllyId(null)
-      invalidate(userId)
+      invalidate()
       toast.success("Ally removed")
     },

Also applies to: 205-245, 261-264

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/allies/allies-page.tsx` around lines 182 - 190, The
invalidate helper in allies-page.tsx only clears the ["user-profile", userId]
cache when called, but several non-remove mutations (send/accept/decline ally
request) do not call it so cached user-profile popovers show stale allyStatus;
update the onSuccess handlers of the mutation functions (the send/accept/decline
mutations in allies-page.tsx) to call invalidate with the affected userId (so it
triggers queryClient.invalidateQueries({ queryKey: ["user-profile",
affectedUserId] })) after each successful mutation, ensuring the cached key used
in user-profile-card.tsx (["user-profile", userId]) is refreshed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/v1/allies/handlers.ts`:
- Around line 346-357: The UPDATE currently only filters by id after a separate
pending check, allowing races; change the db.update call that sets
status="accepted" (the block that calls
db.update(schema.allyRequest).set({...}).where(eq(schema.allyRequest.id,
requestId)).returning()) to include both eq(schema.allyRequest.receiverId,
currentUser.id) and eq(schema.allyRequest.status, "pending") in the WHERE
predicate so the transition is conditional, then check the returned updated
count (or array length) and treat 0 updated rows as a stale/forbidden result
(return a 403 or appropriate error). Apply the identical predicate+0-updates
check to the decline handler that updates status to "declined" so both paths are
safe from races.
- Around line 97-118: The check-then-insert is racy because uniqueness is only
on (senderId, receiverId); change to canonicalize the unordered pair (e.g.,
compute userAId = min(currentUser.id, targetUserId), userBId = max(...)) and add
a DB unique constraint on (userAId, userBId) in the allyRequest table, then
replace the select-then-insert flow with a conflict-safe insert: insert the
request using db.insert(schema.allyRequest).values(...) with the canonical pair
and use ON CONFLICT (userAId, userBId) DO NOTHING (or DO UPDATE to set status)
and RETURNING to get the existing/created row so you don't need the separate
`existing` select; apply the same canonicalization + conflict-safe insert fix to
the other send/accept paths referenced (the other allyRequest insert sites
around the noted ranges).
- Around line 4-11: The import of route types currently uses a relative path
("./routes"); update it to use the project path alias used in apps/api so
tsc-alias can rewrite during build (replace the "./routes" import with the alias
import path for the same module, e.g., import the types from
"@/routes/v1/allies/routes"). Ensure the same exported type names
(AcceptAllyRequestRoute, DeclineAllyRequestRoute, ListAlliesRoute,
ListAllyRequestsRoute, RemoveAllyRoute, SendAllyRequestRoute) are preserved and
that the change is made in handlers.ts where those types are currently imported.

In `@apps/web/src/components/allies/allies-page.tsx`:
- Around line 62-79: The remove control is hidden by default using "opacity-0
group-hover:opacity-100", which hides it from touch and keyboard users; update
the action container in allies-page.tsx (the div wrapping the Buttons) to also
reveal on keyboard focus and non-hover layouts by adding focus/focus-within and
non-hover breakpoint classes (e.g., include group-focus-within:opacity-100 and a
responsive override like sm:opacity-100 or focus-visible variants) so the Remove
button (onClick => onRemove(ally.id)) becomes visible to keyboard and touch
users while preserving hover behavior.
- Around line 162-178: The useQuery calls for allies and ally-requests currently
only destructure data and isPending, so fetch failures fall back to empty arrays
and show empty-state UI; update both useQuery hooks (the ones assigning
allies/alliesLoading and requests/requestsLoading) to also destructure isError
and/or error, then add early error UI branches that render when isError or error
is present (check alliesLoading/requestsLoading first), placing those error
branches before the existing empty-state checks so failed fetches show an error
message instead of the "no data" state.

In `@apps/web/src/hooks/use-message-reactions.ts`:
- Around line 54-61: The currentUser object is recreated each render causing
toggleReactionLocal (defined with useCallback) to be recreated; memoize
currentUser with useMemo so its reference only changes when currentUserId or
currentUserName change, then keep toggleReactionLocal’s dependency array as
[updateMessageInCache, currentUser]; update the code where currentUser is
constructed (the object created on lines around its declaration) to use useMemo
and reference currentUserId/currentUserName so
toggleReactionOptimistically(message, emoji, currentUser) receives a stable
object and toggleReactionLocal stops being unnecessarily recreated.

---

Duplicate comments:
In `@apps/web/src/components/allies/allies-page.tsx`:
- Around line 182-190: The invalidate helper in allies-page.tsx only clears the
["user-profile", userId] cache when called, but several non-remove mutations
(send/accept/decline ally request) do not call it so cached user-profile
popovers show stale allyStatus; update the onSuccess handlers of the mutation
functions (the send/accept/decline mutations in allies-page.tsx) to call
invalidate with the affected userId (so it triggers
queryClient.invalidateQueries({ queryKey: ["user-profile", affectedUserId] }))
after each successful mutation, ensuring the cached key used in
user-profile-card.tsx (["user-profile", userId]) is refreshed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ff8e099f-9138-4db7-b2bc-84537c37bcf4

📥 Commits

Reviewing files that changed from the base of the PR and between 9259153 and 40adda9.

📒 Files selected for processing (6)
  • ROADMAP.md
  • apps/api/src/routes/v1/allies/handlers.ts
  • apps/web/src/components/allies/allies-page.tsx
  • apps/web/src/components/chat/message-item.tsx
  • apps/web/src/hooks/use-message-reactions.ts
  • apps/web/src/lib/realtime-adapter.ts

Comment thread apps/api/src/routes/v1/allies/handlers.ts
Comment thread apps/api/src/routes/v1/allies/handlers.ts
Comment thread apps/api/src/routes/v1/allies/handlers.ts
Comment thread apps/web/src/components/allies/allies-page.tsx
Comment thread apps/web/src/components/allies/allies-page.tsx Outdated
Comment thread apps/web/src/hooks/use-message-reactions.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
apps/web/src/components/allies/allies-page.tsx (1)

65-85: ⚠️ Potential issue | 🟠 Major

Don't keep the ally actions hover-only.

This still hides the only DM/remove controls from touch users, and keyboard users can tab into an invisible action group. Make the buttons visible on non-hover layouts and when the row receives focus.

♿ Suggested fix
-      <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
+      <div className="flex items-center gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/allies/allies-page.tsx` around lines 65 - 85, The
action buttons are hidden by default using "opacity-0 group-hover:opacity-100",
which makes them inaccessible to touch and keyboard users; update the wrapper
div's classes so the controls are visible when the row receives focus or for
non-hover/touch layouts (e.g., replace "opacity-0 group-hover:opacity-100" with
classes that include focus/focus-within and non-hover visibility such as
"opacity-100 group-hover:opacity-100 group-focus-within:opacity-100
focus-within:opacity-100" or otherwise remove the initial opacity-0), keeping
the onClick handlers (onMessage/onRemove), the disabled prop (isRemoving) and
icons (MessageCircle/UserMinus) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/v1/dms/handlers.ts`:
- Around line 114-189: Wrap the whole 1:1 DM lookup+create in a single database
transaction and use a DB-backed uniqueness key for direct-DMs to avoid race
conditions: generate a canonical identifier for the direct pair (e.g., sorted
user IDs joined, stored as dm_key on channel) and add a UNIQUE constraint on
(type='dm', dm_key) in the schema; inside a transaction (using db.transaction or
your DB client), first try to select an existing DM by dm_key, if none attempt
to insert the channel with dm_key and then insert channelMember rows in the same
transaction; on unique-constraint violation (insert conflict) catch the error,
re-select the existing channel within the transaction and return it, and ensure
any failure rolls back so no orphan channel remains. Use the existing symbols
channel, channelMember, allMemberIds, isDirect, db.insert/db.select, and
returnDMResponse when implementing this.
- Around line 13-18: The import statement at the top of handlers.ts uses a
relative path to "./routes" which violates the apps/api guideline to use the `@/`
path alias; replace the current import with an alias import that points to the
same module (e.g., import the types CreateDMRoute, GetDMRoute,
ListDMMessagesRoute, ListDMsRoute from "@/routes") so tsc-alias can rewrite
paths during build and keep apps/api compatible with the build pipeline.

In `@apps/web/src/components/allies/allies-page.tsx`:
- Around line 192-195: The single-ID state removingAllyId is race-prone; change
it to track multiple in-flight removals (e.g., useState<Set<string>> or
useRef<Set<string>>) and update all places that read/write removingAllyId to add
the target ally id before calling the delete, and remove it after the request
settles (success or error). Concretely, replace removingAllyId /
setRemovingAllyId usage with removingAllyIds and
addRemovingAllyId/removeRemovingAllyId helpers, update any button
disabled/loader checks from removingAllyId === id to removingAllyIds.has(id),
and apply the same pattern in the other remove handlers referenced (the blocks
using removingAllyId at lines noted) so concurrent deletes are tracked per-id
rather than clobbered by a single scalar. Ensure cleanup in finally so state is
cleared for that id regardless of outcome.

In `@apps/web/src/components/sidebar/dm-panel/dm-panel.tsx`:
- Around line 50-56: The icon-only button that opens the new DM modal lacks an
accessible name; update the button (the element using onClick={() =>
setNewDMOpen(true)} and rendering the Plus icon) to provide an accessible label
(for example add an aria-label like "New direct message" or include visually
hidden text inside the button) so screen readers can announce its purpose while
preserving the current visual appearance and behavior.

In `@apps/web/src/components/sidebar/dm-panel/new-dm-dialog.tsx`:
- Around line 32-40: The query result is collapsing fetch failures into the
empty state; update the useQuery usage for allies (queryKey ["allies"], queryFn
using apiClient.v1.allies.$get) to surface errors by reading the query's
error/isError (or status) and render an explicit error branch when the request
fails instead of showing the "You don't have any allies yet." empty message;
ensure the component checks isPending, then isError/error and displays a failure
message (or error details) before falling back to the empty allies rendering.

---

Duplicate comments:
In `@apps/web/src/components/allies/allies-page.tsx`:
- Around line 65-85: The action buttons are hidden by default using "opacity-0
group-hover:opacity-100", which makes them inaccessible to touch and keyboard
users; update the wrapper div's classes so the controls are visible when the row
receives focus or for non-hover/touch layouts (e.g., replace "opacity-0
group-hover:opacity-100" with classes that include focus/focus-within and
non-hover visibility such as "opacity-100 group-hover:opacity-100
group-focus-within:opacity-100 focus-within:opacity-100" or otherwise remove the
initial opacity-0), keeping the onClick handlers (onMessage/onRemove), the
disabled prop (isRemoving) and icons (MessageCircle/UserMinus) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 54106d8d-6210-4c0c-9ab2-75c8966bbca1

📥 Commits

Reviewing files that changed from the base of the PR and between 40adda9 and abb12d8.

📒 Files selected for processing (10)
  • apps/api/src/routes/v1/allies/handlers.ts
  • apps/api/src/routes/v1/dms/handlers.ts
  • apps/api/src/routes/v1/dms/index.ts
  • apps/api/src/routes/v1/dms/routes.ts
  • apps/api/src/routes/v1/dms/schema.ts
  • apps/web/src/components/allies/allies-page.tsx
  • apps/web/src/components/sidebar/dm-panel/dm-panel.tsx
  • apps/web/src/components/sidebar/dm-panel/new-dm-dialog.tsx
  • apps/web/src/hooks/use-create-dm.ts
  • apps/web/src/hooks/use-message-reactions.ts

Comment on lines +13 to +18
import type {
CreateDMRoute,
GetDMRoute,
ListDMMessagesRoute,
ListDMsRoute,
} from "./routes"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use the API path alias for these route types.

Keep apps/api imports on @/... so this package continues to build through tsc && tsc-alias.

♻️ Suggested fix
-} from "./routes"
+} from "@/routes/v1/dms/routes"

As per coding guidelines, "Use @/* path alias in apps/api for imports (e.g., import foo from "@/lib/foo"). tsc-alias will rewrite these to relative .js paths in the compiled output."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/routes/v1/dms/handlers.ts` around lines 13 - 18, The import
statement at the top of handlers.ts uses a relative path to "./routes" which
violates the apps/api guideline to use the `@/` path alias; replace the current
import with an alias import that points to the same module (e.g., import the
types CreateDMRoute, GetDMRoute, ListDMMessagesRoute, ListDMsRoute from
"@/routes") so tsc-alias can rewrite paths during build and keep apps/api
compatible with the build pipeline.

Comment thread apps/api/src/routes/v1/dms/handlers.ts Outdated
Comment on lines +192 to +195
const [removingAllyId, setRemovingAllyId] = useState<string | null>(null)

const invalidate = (affectedUserId?: string) => {
void queryClient.invalidateQueries({ queryKey: ["allies"] })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

removingAllyId is still race-prone when deletes overlap.

Because other rows stay clickable, a second remove can start before the first one settles. With only one tracked ID, the later click overwrites the earlier loading state, and whichever request finishes first clears the indicator for any remaining delete. Track a set of in-flight ally IDs, or intentionally serialize removals.

🛠️ Suggested fix
-  const [removingAllyId, setRemovingAllyId] = useState<string | null>(null)
+  const [removingAllyIds, setRemovingAllyIds] = useState<Set<string>>(
+    new Set()
+  )
...
   const removeAlly = useMutation({
     mutationFn: async (userId: string) => {
-      setRemovingAllyId(userId)
       const res = await apiClient.v1.allies[":userId"].$delete({
         param: { userId },
       })
       if (!res.ok) throw new Error("Failed to remove ally")
       return userId
     },
+    onMutate: (userId) => {
+      setRemovingAllyIds((prev) => {
+        const next = new Set(prev)
+        next.add(userId)
+        return next
+      })
+    },
     onSuccess: (userId) => {
-      setRemovingAllyId(null)
       invalidate(userId)
       toast.success("Ally removed")
     },
     onError: () => {
-      setRemovingAllyId(null)
       toast.error("Failed to remove ally")
     },
+    onSettled: (_data, _error, userId) => {
+      if (!userId) return
+      setRemovingAllyIds((prev) => {
+        const next = new Set(prev)
+        next.delete(userId)
+        return next
+      })
+    },
   })
...
-                      isRemoving={removingAllyId === ally.id}
+                      isRemoving={removingAllyIds.has(ally.id)}

Also applies to: 277-295, 389-395

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/allies/allies-page.tsx` around lines 192 - 195, The
single-ID state removingAllyId is race-prone; change it to track multiple
in-flight removals (e.g., useState<Set<string>> or useRef<Set<string>>) and
update all places that read/write removingAllyId to add the target ally id
before calling the delete, and remove it after the request settles (success or
error). Concretely, replace removingAllyId / setRemovingAllyId usage with
removingAllyIds and addRemovingAllyId/removeRemovingAllyId helpers, update any
button disabled/loader checks from removingAllyId === id to
removingAllyIds.has(id), and apply the same pattern in the other remove handlers
referenced (the blocks using removingAllyId at lines noted) so concurrent
deletes are tracked per-id rather than clobbered by a single scalar. Ensure
cleanup in finally so state is cleared for that id regardless of outcome.

Comment thread apps/web/src/components/sidebar/dm-panel/dm-panel.tsx
Comment thread apps/web/src/components/sidebar/dm-panel/new-dm-dialog.tsx Outdated
channel memory

- Wrap 1:1 DM lookup+create in a transaction to prevent duplicate DMs
- Add confirmation dialog before removing an ally
- Remember last visited channel per guild in localStorage and
  auto-navigate
- Remove unused Message Requests button from DM sidebar
- Add error states and accessibility improvements
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/routes/v1/dms/handlers.ts`:
- Around line 142-170: The DM creation transaction (db.transaction around
newChannelId with isDirect check and the candidate query using
channelMember/channel) can race under READ COMMITTED; update the candidate
lookup to acquire row-level locks (use FOR UPDATE / forUpdate on the tx query
that selects candidate.channelId) so concurrent transactions serialize, add a
canonical DM uniqueness constraint in your channels schema (e.g., a dm_key
column derived from sorted member IDs) and update the insert path to catch and
handle unique-violation errors (return the existing channelId when constraint
violated), and/or run this transaction at SERIALIZABLE isolation as a fallback;
apply the locking change to the tx.select(...).innerJoin(...) query and add
handling around the insert that creates the channel inside the same
db.transaction where newChannelId is produced.

In `@apps/web/src/components/allies/allies-page.tsx`:
- Around line 475-477: The current buttons use a shared pending flag
(isPending={acceptRequest.isPending || declineRequest.isPending}) which disables
all incoming request controls; change this to a per-request pending state by
tracking the active request id (e.g., local state like activeRequestId or a map)
and set isPending on each Accept/Decline button based on whether that button's
request id matches the pending id or has pending[id] true; update the
acceptRequest/declineRequest invocation handlers (the functions referenced as
acceptRequest and declineRequest) to set and clear the per-request pending state
around the async call so only the specific request's buttons are disabled.

In `@apps/web/src/routes/_authenticated/`$guildSlug/$channelId.tsx:
- Around line 42-44: The current useEffect writes last-channel immediately and
unguarded, which can persist invalid channel IDs and throw in restricted
environments; update the useEffect so it only runs after the channel is
resolved/validated (e.g., wait for a truthy channel object or isChannelLoaded
flag) and ensure guildSlug and channelId are non-empty, then wrap the storage
call in a try/catch and verify localStorage availability before calling
localStorage.setItem (use symbols: useEffect, guildSlug, channelId,
localStorage.setItem).

In `@apps/web/src/routes/_authenticated/`$guildSlug/index.tsx:
- Around line 12-21: The effect in useEffect reads localStorage.getItem directly
which can throw in restricted environments; wrap the storage access for the key
`last-channel:${guildSlug}` in a safe guard (e.g., check typeof window !==
"undefined" and try/catch around localStorage.getItem) and fall back to null if
unavailable, then only call navigate when lastChannelId is non-null; update the
code around useEffect / localStorage.getItem to perform this guarded read so
navigate is never invoked due to an exception from storage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 390990a9-f587-47c6-babf-9b45a73c6c36

📥 Commits

Reviewing files that changed from the base of the PR and between abb12d8 and 6571f20.

📒 Files selected for processing (6)
  • apps/api/src/routes/v1/dms/handlers.ts
  • apps/web/src/components/allies/allies-page.tsx
  • apps/web/src/components/sidebar/dm-panel/dm-panel.tsx
  • apps/web/src/components/sidebar/dm-panel/new-dm-dialog.tsx
  • apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx
  • apps/web/src/routes/_authenticated/$guildSlug/index.tsx

Comment thread apps/api/src/routes/v1/dms/handlers.ts
Comment thread apps/web/src/components/allies/allies-page.tsx
Comment thread apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx
Comment thread apps/web/src/routes/_authenticated/$guildSlug/index.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx (1)

42-51: ⚠️ Potential issue | 🟡 Minor

Persisting before channel validation still not addressed

The try/catch and window guard are good, but the effect still writes to localStorage before verifying the channel exists (via the data query result). If a user navigates to an invalid channel URL, the invalid ID gets cached, causing redirect loops from the guild home page.

Adding a data dependency and early return would prevent caching invalid channel IDs.

Proposed fix
   useEffect(() => {
-    if (!guildSlug || !channelId) return
+    if (!guildSlug || !channelId || !data) return
     try {
       if (typeof window !== "undefined") {
         localStorage.setItem(`last-channel:${guildSlug}`, channelId)
       }
     } catch {
       // localStorage may be unavailable in restricted environments
     }
-  }, [guildSlug, channelId])
+  }, [guildSlug, channelId, data])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authenticated/`$guildSlug/$channelId.tsx around lines 42
- 51, The effect is persisting channelId to localStorage before the query result
is validated; update the useEffect that references guildSlug and channelId so it
also depends on the query result (data) and returns early unless the channel is
confirmed valid (e.g., data shows the channel exists for the guild).
Specifically, in the useEffect containing
localStorage.setItem(`last-channel:${guildSlug}`, channelId) add data to the
dependency array and add an early guard like if (!data || !channelExistsIn(data,
channelId)) return so you only cache known-good channel IDs and avoid redirect
loops.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/src/routes/_authenticated/`$guildSlug/$channelId.tsx:
- Around line 42-51: The effect is persisting channelId to localStorage before
the query result is validated; update the useEffect that references guildSlug
and channelId so it also depends on the query result (data) and returns early
unless the channel is confirmed valid (e.g., data shows the channel exists for
the guild). Specifically, in the useEffect containing
localStorage.setItem(`last-channel:${guildSlug}`, channelId) add data to the
dependency array and add an early guard like if (!data || !channelExistsIn(data,
channelId)) return so you only cache known-good channel IDs and avoid redirect
loops.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6cd4747b-55d4-4fbf-a405-e9e99a989463

📥 Commits

Reviewing files that changed from the base of the PR and between 6571f20 and c08c0f5.

📒 Files selected for processing (2)
  • apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx
  • apps/web/src/routes/_authenticated/$guildSlug/index.tsx

@BuckyMcYolo BuckyMcYolo merged commit 1330479 into main Mar 22, 2026
1 check passed
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.

1 participant