Skip to content

Commit cecb781

Browse files
ncavaglioneclaude
andcommitted
fix: Bug contains-studio#1 - pinned messages no longer show (edited) label; fix search test parallel flakiness
- Add editedAt field to Message schema (set only on content edit, not pin/unpin) - Update edit route to set editedAt; fix isEdited derivation to use !!msg.editedAt - Add editedAt to ApiMessage interface - Fix Bug contains-studio#4 search test: add random suffix to search term, assert within data-testid="search-results-dropdown" - Update bugs.md to reflect remaining bugs (contains-studio#7 Files tab, contains-studio#8 Typing indicators) - All 63 Playwright tests and 207 backend tests pass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a83d04d commit cecb781

File tree

7 files changed

+55
-237
lines changed

7 files changed

+55
-237
lines changed

backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ model Message {
5959
isPinned Boolean @default(false)
6060
pinnedBy Int?
6161
pinnedAt DateTime?
62+
editedAt DateTime?
6263
createdAt DateTime @default(now())
6364
updatedAt DateTime @default(now()) @updatedAt
6465
deletedAt DateTime?

backend/src/routes/threads.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ router.patch('/:id', authMiddleware, async (req: AuthRequest, res: Response) =>
155155

156156
const updatedMessage = await prisma.message.update({
157157
where: { id: messageId },
158-
data: { content },
158+
data: { content, editedAt: new Date() },
159159
include: {
160160
user: {
161161
select: { id: true, name: true, email: true, avatar: true },

bugs.md

Lines changed: 14 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
| 4 | Create Channel | ✅ Pass |
1818
| 5 | Browse Channels | ✅ Pass |
1919
| 6 | Join Channel | ✅ Pass |
20-
| 7 | Leave Channel | ❌ Bug #4 |
20+
| 7 | Leave Channel | ✅ Pass |
2121
| 8 | Send Message | ✅ Pass |
2222
| 9 | Message History | ✅ Pass |
2323
| 10 | Typing Indicators | ❌ Bug #8 |
24-
| 11 | File Upload | ⚠️ Partial — Bug #6 |
25-
| 12 | Threads | ✅ Pass (minor Bug #5) |
24+
| 11 | File Upload | ✅ Pass |
25+
| 12 | Threads | ✅ Pass |
2626
| 13 | Search | ✅ Pass |
2727
| 14 | Pinned Messages | ⚠️ Partial — Bug #1 |
2828

@@ -41,132 +41,10 @@
4141
Only messages whose **content** was modified should display the `(edited)` badge.
4242

4343
### Actual
44-
Pinning a message triggers a database `UPDATE` which changes `updatedAt`. The frontend compares `updatedAt > createdAt` to decide whether to show `(edited)`, so every pinned message is incorrectly labeled as edited — even if the text was never changed.
44+
Pinning a message triggers a database `UPDATE` which changes `updatedAt`. The frontend (`src/lib/api.ts:46`) computes `isEdited: msg.updatedAt !== msg.createdAt`, so every pinned message is incorrectly labeled as edited — even if the text was never changed.
4545

4646
### Root Cause
47-
The "(edited)" check does not distinguish between a content edit and a metadata update (e.g. `isPinned` toggle). The backend should either use a separate `editedAt` field for content edits, or the pin operation should preserve the existing `updatedAt`.
48-
49-
---
50-
51-
## Bug #2 — Emoji reactions stored as shortcodes render as raw text
52-
53-
**Severity:** High
54-
**Feature:** Reactions
55-
56-
### Steps to Reproduce
57-
1. Open any channel that has reactions inserted via the seed script (or the database directly)
58-
2. Observe the reaction badges under those messages
59-
60-
### Expected
61-
Reactions display as Unicode emoji with a count — e.g. `👍 2`, `🔥 1`, `🎉 1`
62-
63-
### Actual
64-
Reactions display as raw shortcode text — e.g. `+1 2`, `fire 1`, `tada1`, `thinking_gl...`, `confetti_ball`
65-
66-
### Note
67-
Reactions **added via the emoji picker in the UI** display correctly because the picker sends the actual Unicode character. The bug only affects reactions stored as shortcode strings. The frontend renders whatever string is in the database without any shortcode → Unicode conversion.
68-
69-
### Root Cause
70-
The reaction display component has no emoji lookup/mapping step. Any reaction stored as a shortcode (`:fire:`, `+1`, etc.) will render as plain text.
71-
72-
---
73-
74-
## Bug #3`TypeError` in `fetchDirectMessages` spams the console
75-
76-
**Severity:** Critical
77-
**Feature:** Direct Messages
78-
79-
### Steps to Reproduce
80-
1. Log in as any user
81-
2. Open the browser DevTools console
82-
3. Wait a few seconds
83-
84-
### Expected
85-
No errors; DMs load silently in the background.
86-
87-
### Actual
88-
Hundreds of repeated errors per minute fill the console:
89-
90-
```
91-
Failed to fetch DMs: TypeError: Cannot read properties of undefined (reading 'id')
92-
at useChannelStore.ts:13 (Array.map callback)
93-
at fetchDirectMessages (useChannelStore.ts:12)
94-
```
95-
96-
Additionally, `fetchMessages` is polled against channels the user is **not** a member of, producing a second stream of errors:
97-
98-
```
99-
Failed to fetch messages: ApiError: You must be a member of this channel
100-
at useMessageStore.ts:57
101-
```
102-
103-
Both errors repeat on every polling interval, flooding the console and wasting CPU.
104-
105-
### Root Cause
106-
- `fetchDirectMessages` maps over an API response that can contain `undefined` entries without a null-guard.
107-
- The channel polling loop iterates over **all visible channels** rather than only the channels the user has joined, causing 403 responses for private/unjoined channels.
108-
109-
---
110-
111-
## Bug #4 — Leave channel has no UI entry point
112-
113-
**Severity:** High
114-
**Feature:** Leave Channel
115-
116-
### Steps to Reproduce
117-
1. Log in and join any channel
118-
2. Try to find a "Leave channel" option in the UI — check the channel header dropdown (`˅`), the `` menu, hover actions, right-click on the channel name, etc.
119-
120-
### Expected
121-
A "Leave channel" option accessible from somewhere in the channel UI.
122-
123-
### Actual
124-
No UI element exists anywhere. The feature is completely missing from the interface.
125-
126-
### Note
127-
The backend endpoint `POST /channels/:id/leave` is fully implemented, and the frontend API wrapper `leaveChannel(id)` exists in `src/lib/api.ts` — but **no component calls it**. The plumbing is there; only the UI trigger is missing.
128-
129-
---
130-
131-
## Bug #5 — Thread reply input ignores the Enter key
132-
133-
**Severity:** Low
134-
**Feature:** Threads
135-
136-
### Steps to Reproduce
137-
1. Click "N replies" on any message to open the Thread panel
138-
2. Click the reply input field and type a message
139-
3. Press Enter
140-
141-
### Expected
142-
The reply is sent (consistent with the main message box, which displays the hint "Enter to send, Shift + Enter for new line").
143-
144-
### Actual
145-
Nothing happens on Enter. The user must click the blue send button manually.
146-
147-
### Note
148-
This is a minor inconsistency between the thread reply input (plain `<input>`) and the main Quill-based rich-text editor. The main editor correctly handles Enter-to-send.
149-
150-
---
151-
152-
## Bug #6 — File attachment disappears without sending a message
153-
154-
**Severity:** High
155-
**Feature:** File Upload
156-
157-
### Steps to Reproduce
158-
1. In any channel, click the `+` button in the message composer
159-
2. Select a file — it appears as a badge in the composer (e.g. `qa-test.png ×`)
160-
3. Click the send button
161-
162-
### Expected
163-
A message containing the file attachment appears in the channel.
164-
165-
### Actual
166-
The file is uploaded to the server (`POST /files` returns `201 Created`) and the attachment badge disappears from the composer, but **no message is ever created**. `POST /channels/:id/messages` is never called after the file upload completes.
167-
168-
### Root Cause
169-
The `handleFileSelect` in `MessageInput.tsx` uploads the file immediately on selection and stores the result in `pendingFiles` state. However, the send action does not appear to include `pendingFiles` in the message payload, so the uploaded file ID is silently discarded on send.
47+
`api.ts` derives `isEdited` by comparing `updatedAt` vs `createdAt`. The backend should either use a separate `editedAt` field set only on content edits, or the pin operation should preserve the existing `updatedAt`.
17048

17149
---
17250

@@ -186,7 +64,7 @@ A panel or filtered view listing files shared in the channel — similar to how
18664
Clicking "Files" has no visible effect. The main message view is unchanged and no files panel appears.
18765

18866
### Note
189-
The "Pins" tab works correctly and opens a right-side panel. The "Files" tab appears to be a stub with no implementation behind it.
67+
The "Pins" tab works correctly and opens a right-side panel. The "Files" tab only sets `activeTab` state in `MessageHeader.tsx` with no `onToggleFiles` prop or panel component behind it.
19068

19169
---
19270

@@ -219,110 +97,15 @@ The feature is 0% implemented on the frontend side.
21997

22098
---
22199

222-
## Bug #9 — Pinned message has no visual background highlight
223-
224-
**Severity:** Low
225-
**Feature:** Pinned Messages
226-
227-
### Steps to Reproduce
228-
1. Pin any message in a channel
229-
2. View the message in the channel feed
230-
231-
### Expected
232-
The background of a pinned message row should be a light amber/orange (`#FEF9ED`) to visually distinguish it from regular messages.
233-
234-
### Actual
235-
Pinned messages render with the same white background as all other messages. The only pinned indicator is the small orange pin icon/label, which can be easy to miss.
236-
237-
### Root Cause
238-
`Message.tsx` applies a fixed `hover:bg-[#F8F8F8]` class but never conditionally applies a pinned background. The `message.isPinned` flag is available; it just isn't used for background styling.
239-
240-
---
241-
242-
## Bug #10 — Video call button in message composer should be removed
243-
244-
**Severity:** Low
245-
**Feature:** Message Input
246-
247-
### Steps to Reproduce
248-
1. Open any channel
249-
2. Look at the bottom toolbar of the message composer
250-
251-
### Expected
252-
Only controls that have working functionality should appear. Video calls are not supported.
253-
254-
### Actual
255-
A video-camera icon (`Video` from `lucide-react`) is rendered in the composer toolbar. Clicking it does nothing. It creates a misleading affordance.
256-
257-
### Root Cause
258-
`MessageInput.tsx` imports and renders a `<Video>` icon button that has no `onClick` handler and no underlying feature.
259-
260-
---
261-
262-
## Bug #11 — Reaction emoji glyphs render too small inside the pill
263-
264-
**Severity:** Medium
265-
**Feature:** Reactions
266-
267-
### Steps to Reproduce
268-
1. Add any emoji reaction to a message
269-
2. Observe the reaction pill/badge
270-
271-
### Expected
272-
The emoji character and its accompanying count number are comfortably legible — at least `16px` for the glyph and consistent sizing for the count.
273-
274-
### Actual
275-
The emoji `<span>` is constrained to a `w-4 h-4` (16 px) box, which clips or compresses many multi-codepoint emoji. The count number is rendered at `text-[12px]`, making the whole badge feel tiny. Both should be visually larger while the oval pill container itself can stay the same proportions.
276-
277-
### Root Cause
278-
In `MessageReactions.tsx`, the emoji span has `w-4 h-4 flex items-center justify-center` with no explicit `font-size`, and the count uses `font-normal` at the container's default `text-[12px]`.
279-
280-
---
281-
282-
## Bug #12 — Channel star/favorite button has no effect
283-
284-
**Severity:** High
285-
**Feature:** Channel Navigation
286-
287-
### Steps to Reproduce
288-
1. Open any channel
289-
2. Click the ☆ star icon next to the channel name in the header
290-
3. Navigate away and back, or look at the sidebar
291-
292-
### Expected
293-
- Clicking the star toggles a "starred/favorited" state for that channel.
294-
- The star icon fills in (★) to confirm the toggle.
295-
- A **Starred** section appears at the top of the channel list in the sidebar listing all starred channels.
296-
- The state persists across navigation (stored in `localStorage` or the backend).
297-
298-
### Actual
299-
Clicking the star does nothing. No visual feedback, no state change, no sidebar section.
300-
301-
### Root Cause
302-
`MessageHeader.tsx` renders the `<Star>` icon button with no `onClick` handler and no state. The channel store has no `isStarred` field and no `toggleStar` action.
303-
304-
---
305-
306100
## Summary
307101

308102
| Severity | Count | Bugs |
309103
|----------|-------|------|
310-
| Critical | 1 | #3 |
311-
| High | 5 | #2, #4, #6, #8, #12 |
312-
| Medium | 3 | #1, #7, #11 |
313-
| Low | 3 | #5, #9, #10 |
314-
| **Total** | **12** | |
315-
316-
### Recommended Fix Priority
317-
1. **Bug #3** (Critical) — Fix the null-guard crash and the over-broad polling; this generates hundreds of errors per session.
318-
2. **Bug #8** (High) — Add `typing:start`/`typing:stop` emit in `MessageInput.tsx` and a listener + UI indicator.
319-
3. **Bug #6** (High) — Ensure `pendingFiles` IDs are included in the `sendMessage` payload.
320-
4. **Bug #4** (High) — Wire the existing `leaveChannel()` API call to a UI element (e.g. channel header `` menu).
321-
5. **Bug #12** (High) — Implement star/favorite toggle with a Starred section in the sidebar.
322-
6. **Bug #2** (High) — Add a shortcode → Unicode emoji mapping in the reaction display component.
323-
7. **Bug #7** (Medium) — Implement the Files side panel (similar to Pins panel).
324-
8. **Bug #11** (Medium) — Make reaction emoji glyphs and count numbers larger inside the pill.
325-
9. **Bug #1** (Medium) — Use a dedicated `editedAt` field (set only on content edits) instead of `updatedAt`.
326-
10. **Bug #5** (Low) — Add Enter-to-send handling to the thread reply input.
327-
11. **Bug #9** (Low) — Apply `#FEF9ED` background to pinned message rows.
328-
12. **Bug #10** (Low) — Remove the non-functional video call button from the message composer.
104+
| High | 1 | #8 |
105+
| Medium | 2 | #1, #7 |
106+
| **Total** | **3** | |
107+
108+
### Remaining Fix Priority
109+
1. **Bug #8** (High) — Add `typing:start`/`typing:stop` emit in `MessageInput.tsx` and a listener + UI indicator.
110+
2. **Bug #7** (Medium) — Implement the Files side panel (similar to Pins panel).
111+
3. **Bug #1** (Medium) — Use a dedicated `editedAt` field (set only on content edits) instead of `updatedAt`.

frontend/src/components/Messages/MessageHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function MessageHeader({ channel, showMembers, onToggleMembers, onToggleP
137137
/>
138138
{/* Search Results Dropdown */}
139139
{showResults && (
140-
<div className="absolute right-0 top-8 z-50 w-[360px] max-h-[400px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg">
140+
<div data-testid="search-results-dropdown" className="absolute right-0 top-8 z-50 w-[360px] max-h-[400px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg">
141141
{isSearching ? (
142142
<div className="p-4 text-center text-sm text-gray-500">Searching...</div>
143143
) : searchResults.length === 0 ? (

frontend/src/lib/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export interface ApiMessage {
119119
isPinned?: boolean;
120120
pinnedBy?: number | null;
121121
pinnedAt?: string | null;
122+
editedAt?: string | null;
122123
createdAt: string;
123124
updatedAt: string;
124125
deletedAt: string | null;

frontend/src/stores/useMessageStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function transformApiMessage(msg: api.ApiMessage): Message {
4343
url: f.url,
4444
})),
4545
threadCount: msg._count?.replies ?? 0,
46-
isEdited: msg.updatedAt !== msg.createdAt,
46+
isEdited: !!msg.editedAt,
4747
isPinned: msg.isPinned ?? false,
4848
};
4949
}

frontend/tests/bugfixes.spec.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ test.describe('Bug #4: Search functionality', () => {
101101
await expect(page.locator('button').filter({ hasText: 'general' })).toBeVisible({ timeout: 10_000 });
102102
await page.locator('button').filter({ hasText: 'general' }).click();
103103
await expect(page.locator('.ql-editor')).toBeVisible();
104-
const searchTerm = `searchable-${Date.now()}`;
104+
105+
// Add random suffix to avoid collision between parallel test workers
106+
const searchTerm = `srch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
105107
await sendMessage(page, searchTerm);
106108
await waitForMessage(page, searchTerm);
107109

@@ -110,8 +112,10 @@ test.describe('Bug #4: Search functionality', () => {
110112
await searchInput.fill(searchTerm);
111113
await searchInput.press('Enter');
112114

113-
// Search results should appear
114-
await expect(page.getByText(searchTerm).first()).toBeVisible({ timeout: 5_000 });
115+
// Wait for search results dropdown to appear and contain our message
116+
const searchDropdown = page.locator('[data-testid="search-results-dropdown"]');
117+
await expect(searchDropdown).toBeVisible({ timeout: 10_000 });
118+
await expect(searchDropdown.getByText(searchTerm)).toBeVisible({ timeout: 10_000 });
115119

116120
// Press Escape to clear
117121
await searchInput.press('Escape');
@@ -333,6 +337,35 @@ test.describe('Bug #10: No video icon in message composer', () => {
333337
});
334338
});
335339

340+
test.describe('Bug #1: Pinned message does not show (edited) label', () => {
341+
test('pinning a message does not make it show (edited)', async ({ page }) => {
342+
const email = uniqueEmail();
343+
await register(page, 'PinEdit User', email, 'password123');
344+
345+
await expect(page.locator('button').filter({ hasText: 'general' })).toBeVisible({ timeout: 10_000 });
346+
await page.locator('button').filter({ hasText: 'general' }).click();
347+
await expect(page.locator('.ql-editor')).toBeVisible();
348+
349+
const msg = `pin-edited-${Date.now()}`;
350+
await sendMessage(page, msg);
351+
await waitForMessage(page, msg);
352+
353+
const messageRow = page.locator('.group.relative.flex.px-5').filter({ hasText: msg });
354+
await messageRow.hover();
355+
356+
// Open the ⋮ more menu (4th button in hover toolbar)
357+
const toolbar = messageRow.locator('.absolute.-top-4.right-5').first();
358+
await toolbar.locator('button').nth(3).click();
359+
360+
// Pin the message
361+
await page.getByRole('button', { name: /^pin message$/i }).click();
362+
363+
// The "(edited)" badge must NOT appear — pinning is not a content edit
364+
await page.waitForTimeout(500);
365+
await expect(messageRow.getByText('(edited)')).toHaveCount(0);
366+
});
367+
});
368+
336369
test.describe('Bug #9: Pinned message has orange background', () => {
337370
test('pinned message row shows #FEF9ED background', async ({ page }) => {
338371
const email = uniqueEmail();

0 commit comments

Comments
 (0)