Skip to content

feat: infinite scroll on blabsy and pictique#921

Merged
coodos merged 6 commits intomainfrom
feat/blabsy-and-pictique-infinite-chat-scroll
Mar 15, 2026
Merged

feat: infinite scroll on blabsy and pictique#921
coodos merged 6 commits intomainfrom
feat/blabsy-and-pictique-infinite-chat-scroll

Conversation

@coodos
Copy link
Copy Markdown
Contributor

@coodos coodos commented Mar 15, 2026

Description of change

  • Add infinite scroll on chat for both pictique and blabsy

Issue Number

Type of change

  • Update (a change which updates existing functionality)

How the change has been tested

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scroll for older messages; explicit endpoint to fetch messages before a given message.
    • Message read receipts with debounced marking.
  • UX Improvements

    • Pagination indicators: loading state and "No more messages".
    • Improved scroll behavior: auto-scroll only for newest messages; preserves position when loading older messages.
    • SSE initial payload now includes metadata for smoother initial load.
  • Performance

    • Reduced initial message batch sizes for snappier loads.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

Adds cursor-based pagination and infinite-scroll to chats: client scroll detection, loading older messages with scroll-position restoration, SSE initial payload metadata (type/hasMore/total), new server endpoints and service methods for paginated retrieval, and client-side dedupe/grouping for streamed and historical messages.

Changes

Cohort / File(s) Summary
Blabsy Client — Chat Window
platforms/blabsy/client/src/components/chat/chat-window.tsx
Added scroll container, refs, useLayoutEffect to save/restore scroll position; auto-scroll only for newest messages; onScroll triggers loadOlderMessages; UI shows "No more messages" and loading state; consumes hasMoreMessages, loadingOlderMessages, loadOlderMessages from chat context.
Blabsy Client — Chat Context
platforms/blabsy/client/src/lib/context/chat-context.tsx
Introduced pagination constants/state (MESSAGES_PER_PAGE, oldestDocSnapshot, hasMoreMessages, loadingOlderMessages); split realtimeMessages vs olderMessages with merged deduped view; added loadOlderMessages (cursor-based, startAfter) and exposed pagination fields; createNewChat signature extended to accept optional description.
Pictique Server — Controller & Service
platforms/pictique/api/src/controllers/MessageController.ts, platforms/pictique/api/src/services/ChatService.ts
Added getMessagesBefore controller; added getLatestMessages and getChatMessagesBefore service methods; reduced default limits to 30; implemented cursor-based queries (LessThan/Brackets), hasMore calculation, and SSE initial payload now includes { type, messages, total, hasMore }.
Pictique Server — Routes
platforms/pictique/api/src/index.ts
Registered authenticated route GET /api/chats/:chatId/messages/beforemessageController.getMessagesBefore.
Pictique Client — Messages Page
platforms/pictique/client/src/routes/(protected)/messages/[id]/+page.svelte
Reworked SSE handling to accept initial metadata and streaming messages; added transform/dedupe/grouping pipeline; added read receipts (debounced mark-as-read); introduced hasMore, oldestMessageId, loadingOlder and loadOlderMessages with scroll-position preservation; bound onscroll to trigger older-message fetch; UI shows loading indicator and "No more messages".

Sequence Diagram

sequenceDiagram
    participant User
    participant ClientUI as Client UI
    participant ChatContext as Chat Context / Client Service
    participant API
    participant DB as Database

    User->>ClientUI: Scroll to top
    ClientUI->>ClientUI: handleScroll (near top)
    ClientUI->>ChatContext: loadOlderMessages(chatId, beforeId)
    ChatContext->>ChatContext: set loadingOlderMessages = true
    ChatContext->>API: GET /api/chats/:id/messages/before?before=beforeId&limit=30
    API->>DB: Query messages before cursor (createdAt < cursor OR equal with id <)
    DB-->>API: Return messages (limit+1) + total info
    API-->>ChatContext: { messages, hasMore, total }
    ChatContext->>ClientUI: prepend olderMessages + savedScrollInfo
    ClientUI->>ClientUI: restore scroll position (no auto-scroll)
    ClientUI->>User: Render older messages
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • sosweetham

Poem

🐰 I nudge the scroll, both old and new,

I stash the place so you don't lose view;
I fetch the past in tidy lots of thirty,
I stream the new and mark the read—oh so flirty;
Hop, scroll, restore — messages snug and true.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description follows the template structure with all required sections present. However, the 'How the change has been tested' section is completely empty, providing no details about testing methodology or verification. Please provide details in 'How the change has been tested' section explaining what testing was performed to verify the infinite scroll functionality works correctly on both platforms.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: infinite scroll on blabsy and pictique' accurately and concisely summarizes the main change - adding infinite scroll functionality to both chat applications.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/blabsy-and-pictique-infinite-chat-scroll
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@coodos coodos marked this pull request as draft March 15, 2026 19:56
Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (2)
platforms/blabsy/client/src/components/chat/chat-window.tsx (1)

396-405: Minor UX consideration: "No more messages" shown at top of empty conversations.

When hasMoreMessages is false but the chat has few or no messages, the "No more messages" text still appears. This is technically correct but might be confusing UX for new conversations. Consider conditionally hiding it when there are no messages at all:

-{!hasMoreMessages && (
+{!hasMoreMessages && messages?.length > 0 && (
     <p className='text-center text-xs text-gray-400 dark:text-gray-500 my-2'>
         No more messages
     </p>
 )}

This is optional polish.

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

In `@platforms/blabsy/client/src/components/chat/chat-window.tsx` around lines 396
- 405, The "No more messages" line is shown even for empty conversations; update
the conditional that renders it to also check the messages array length (e.g.,
only show when hasMoreMessages is false AND messages.length > 0) so new/empty
chats don't display that text; modify the JSX around hasMoreMessages (in
chat-window.tsx where loadingOlderMessages and the Loading component are
rendered) to include this additional messages check.
platforms/pictique/api/src/services/ChatService.ts (1)

270-302: Potential edge case with identical createdAt timestamps.

The cursor-based pagination uses LessThan(cursorMessage.createdAt) which could skip messages if multiple messages share the same createdAt timestamp. This is unlikely but possible in high-throughput scenarios.

Consider adding a secondary sort criterion (e.g., message ID) to ensure deterministic ordering:

 const messages = await this.messageRepository.find({
     where: {
         chat: { id: chatId },
         createdAt: LessThan(cursorMessage.createdAt),
     },
     relations: ["sender", "readStatuses", "readStatuses.user"],
-    order: { createdAt: "DESC" },
+    order: { createdAt: "DESC", id: "DESC" },
     take: limit + 1,
 });

This is a minor edge case given typical message rates, but worth noting for robustness.

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

In `@platforms/pictique/api/src/services/ChatService.ts` around lines 270 - 302,
In getChatMessagesBefore, the current WHERE uses only
LessThan(cursorMessage.createdAt) which can skip messages with identical
createdAt; update the query used in messageRepository.find (and the cursor
lookup via cursorMessage) to implement a deterministic tie-breaker: include a
compound condition such as (createdAt < cursorCreatedAt) OR (createdAt =
cursorCreatedAt AND id < beforeId), add a secondary order by id DESC to the
existing order by createdAt DESC, and keep the same take/hasMore logic so
pagination remains correct when timestamps are identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platforms/blabsy/client/src/lib/context/chat-context.tsx`:
- Line 42: The ChatContext type for createNewChat is inconsistent with its
implementation: update the type signature in chat-context to match the
implementation by changing createNewChat: (participants: string[], name?:
string) => Promise<string> to include the optional description parameter
(createNewChat: (participants: string[], name?: string, description?: string) =>
Promise<string>), or alternatively remove the description?: string parameter
from the implementation (the function around the createNewChat definition) so
both declaration and implementation match; ensure you update any related usages
to the new signature if needed.

In `@platforms/pictique/api/src/controllers/MessageController.ts`:
- Around line 219-246: getMessagesBefore currently skips verifying the
requesting user is a chat participant; add the same participant check used in
getChat so only members can fetch messages. Before calling
this.chatService.getChatMessagesBefore, retrieve the chat (or call a service
method like this.chatService.isParticipant / this.chatService.getChat) and
ensure req.user.id is listed as a participant; if not, return
res.status(403).json({ error: "Forbidden" }) and only proceed to call
this.chatService.getChatMessagesBefore when the user is authorized.

In `@platforms/pictique/client/src/routes/`(protected)/messages/[id]/+page.svelte:
- Around line 68-69: The variable isOwn is currently set with inverted logic
(const isOwn = sender.id !== userId) causing messages from other users to be
treated as "own"; change the comparison to check equality (sender.id === userId)
so isOwn is true only when the message sender matches userId; update the
assignment near m.sender and review any usage of isOwn in the component
rendering to ensure alignment with the corrected boolean.
- Line 157: The post to mark messages read is currently invoked on every SSE
event via apiClient.post(`/api/chats/${id}/messages/read`); change this so the
read-receipt is sent once after processing the initial batch and thereafter only
once per batch (e.g., debounce/throttle or send after state update that merges
multiple events) inside the SSE event handler, and add error handling (try/catch
or promise .catch) around apiClient.post to log or handle failures; locate the
SSE handler where apiClient.post is called and replace the unconditional
per-event call with a single-call-after-processing strategy that references the
same id param and preserves existing state-updating logic.
- Around line 116-162: watchEventStream opens an EventSource but never closes it
on component unmount; modify watchEventStream to expose or return a cleanup that
closes the EventSource and removes handlers (eventSource.close() and nullifying
onmessage/onopen/onerror/onclose) and then call that cleanup from Svelte's
onDestroy; alternatively store eventSource in a component-scoped variable and in
onDestroy(() => { eventSource?.close(); eventSource = null; }) ensure you close
the SSE and detach handlers used in watchEventStream (references:
watchEventStream, eventSource, onmessage, onopen, onclose, onDestroy).

---

Nitpick comments:
In `@platforms/blabsy/client/src/components/chat/chat-window.tsx`:
- Around line 396-405: The "No more messages" line is shown even for empty
conversations; update the conditional that renders it to also check the messages
array length (e.g., only show when hasMoreMessages is false AND messages.length
> 0) so new/empty chats don't display that text; modify the JSX around
hasMoreMessages (in chat-window.tsx where loadingOlderMessages and the Loading
component are rendered) to include this additional messages check.

In `@platforms/pictique/api/src/services/ChatService.ts`:
- Around line 270-302: In getChatMessagesBefore, the current WHERE uses only
LessThan(cursorMessage.createdAt) which can skip messages with identical
createdAt; update the query used in messageRepository.find (and the cursor
lookup via cursorMessage) to implement a deterministic tie-breaker: include a
compound condition such as (createdAt < cursorCreatedAt) OR (createdAt =
cursorCreatedAt AND id < beforeId), add a secondary order by id DESC to the
existing order by createdAt DESC, and keep the same take/hasMore logic so
pagination remains correct when timestamps are identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f71f54d-e3ed-46ff-8aaf-c40de0cd776d

📥 Commits

Reviewing files that changed from the base of the PR and between 4bfa4b9 and 500bec6.

📒 Files selected for processing (6)
  • platforms/blabsy/client/src/components/chat/chat-window.tsx
  • platforms/blabsy/client/src/lib/context/chat-context.tsx
  • platforms/pictique/api/src/controllers/MessageController.ts
  • platforms/pictique/api/src/index.ts
  • platforms/pictique/api/src/services/ChatService.ts
  • platforms/pictique/client/src/routes/(protected)/messages/[id]/+page.svelte

@coodos coodos marked this pull request as ready for review March 15, 2026 20:46
Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (2)
platforms/blabsy/client/src/components/chat/chat-window.tsx (1)

401-410: Consider reordering "No more messages" indicator for better UX.

The "No more messages" indicator appears above the loading spinner when both conditions could theoretically be true during state transitions. Consider placing the loading indicator first to ensure it's always visible at the very top during loads.

💡 Suggested reorder
-{!hasMoreMessages && messages.length > 0 && (
-    <p className='text-center text-xs text-gray-400 dark:text-gray-500 my-2'>
-        No more messages
-    </p>
-)}
 {loadingOlderMessages && (
     <div className='flex justify-center my-2'>
         <Loading className='h-6 w-6' />
     </div>
 )}
+{!hasMoreMessages && messages.length > 0 && (
+    <p className='text-center text-xs text-gray-400 dark:text-gray-500 my-2'>
+        No more messages
+    </p>
+)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/blabsy/client/src/components/chat/chat-window.tsx` around lines 401
- 410, The "No more messages" notice can be obscured during state transitions
because it is rendered before the loading spinner; in ChatWindow component
reorder the conditional rendering so that loadingOlderMessages (and the Loading
component) is rendered before the hasMoreMessages && messages check, i.e. move
the loading block above the "No more messages" block so the Loading spinner is
always shown at the top during loads; adjust render order around the
loadingOlderMessages, hasMoreMessages, and messages checks accordingly.
platforms/pictique/api/src/services/ChatService.ts (1)

6-6: Unused import: LessThan is imported but not used.

The LessThan operator is imported but the cursor-based pagination uses Brackets with raw comparisons instead.

🧹 Proposed fix to remove unused import
-import { In, LessThan, Brackets } from "typeorm";
+import { In, Brackets } from "typeorm";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/pictique/api/src/services/ChatService.ts` at line 6, Remove the
unused import "LessThan" from the import list in ChatService.ts; update the
import line that currently reads "import { In, LessThan, Brackets } from
'typeorm';" to only import the used symbols (e.g., "In" and "Brackets") so there
are no unused imports remaining.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@platforms/blabsy/client/src/components/chat/chat-window.tsx`:
- Around line 401-410: The "No more messages" notice can be obscured during
state transitions because it is rendered before the loading spinner; in
ChatWindow component reorder the conditional rendering so that
loadingOlderMessages (and the Loading component) is rendered before the
hasMoreMessages && messages check, i.e. move the loading block above the "No
more messages" block so the Loading spinner is always shown at the top during
loads; adjust render order around the loadingOlderMessages, hasMoreMessages, and
messages checks accordingly.

In `@platforms/pictique/api/src/services/ChatService.ts`:
- Line 6: Remove the unused import "LessThan" from the import list in
ChatService.ts; update the import line that currently reads "import { In,
LessThan, Brackets } from 'typeorm';" to only import the used symbols (e.g.,
"In" and "Brackets") so there are no unused imports remaining.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 366349e4-0894-4d00-84dd-674470459294

📥 Commits

Reviewing files that changed from the base of the PR and between 500bec6 and 9036d5d.

📒 Files selected for processing (5)
  • platforms/blabsy/client/src/components/chat/chat-window.tsx
  • platforms/blabsy/client/src/lib/context/chat-context.tsx
  • platforms/pictique/api/src/controllers/MessageController.ts
  • platforms/pictique/api/src/services/ChatService.ts
  • platforms/pictique/client/src/routes/(protected)/messages/[id]/+page.svelte

Copy link
Copy Markdown
Contributor

@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: 2

♻️ Duplicate comments (1)
platforms/pictique/client/src/routes/(protected)/messages/[id]/+page.svelte (1)

79-80: ⚠️ Potential issue | 🔴 Critical

Inverted isOwn logic will cause messages to display on the wrong side.

The condition sender.id !== userId sets isOwn to true when the message is not from the current user. This is semantically inverted—isOwn should be true when the sender is the current user.

🐛 Proposed fix
 const sender = m.sender as Record<string, string>;
-const isOwn = sender.id !== userId;
+const isOwn = sender.id === userId;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/pictique/client/src/routes/`(protected)/messages/[id]/+page.svelte
around lines 79 - 80, The isOwn flag is inverted: currently it uses sender.id
!== userId which marks other users as "own"; change the condition in the message
rendering code (the sender variable and isOwn assignment in +page.svelte) to
compare equality (sender.id === userId) so isOwn is true when the message sender
matches userId; update any places that rely on isOwn (render logic or CSS class
toggles) to use the corrected boolean.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platforms/pictique/client/src/routes/`(protected)/messages/[id]/+page.svelte:
- Around line 176-181: The handleSend function clears messageValue regardless of
POST success and gives no user feedback on failure; wrap the apiClient.post call
in a try/catch, introduce a sending state (e.g., isSending) to prevent duplicate
sends, only clear messageValue after a successful response, and surface errors
to the user (e.g., set an errorMessage state or call the app's
toast/notification helper) and re-enable input on failure; reference the
handleSend function, apiClient.post(`/api/chats/${id}/messages`), and
messageValue when making these changes.
- Around line 127-174: The SSE connection in watchEventStream currently lacks an
onerror handler so failures are silent; add eventSource.onerror to log the
error, set a local connection state (e.g., connectionError or sseError) so the
UI can stop any loading spinner, and gracefully close/cleanup eventSourceRef on
error; implement simple retry logic that closes the current EventSource, waits
with exponential backoff (or fixed delay) and re-invokes watchEventStream to
reconnect (ensure you clear previous handlers to avoid leaks), and call
markMessagesRead/update historyLoaded or other UI flags as appropriate so users
see feedback when the stream drops.

---

Duplicate comments:
In `@platforms/pictique/client/src/routes/`(protected)/messages/[id]/+page.svelte:
- Around line 79-80: The isOwn flag is inverted: currently it uses sender.id !==
userId which marks other users as "own"; change the condition in the message
rendering code (the sender variable and isOwn assignment in +page.svelte) to
compare equality (sender.id === userId) so isOwn is true when the message sender
matches userId; update any places that rely on isOwn (render logic or CSS class
toggles) to use the corrected boolean.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a923bb0-f93e-4aba-bba8-bd4bc97e64a1

📥 Commits

Reviewing files that changed from the base of the PR and between 9036d5d and 41f54c7.

📒 Files selected for processing (1)
  • platforms/pictique/client/src/routes/(protected)/messages/[id]/+page.svelte

Comment on lines +127 to +174
async function watchEventStream() {
const sseUrl = new URL(
`/api/chats/${id}/events?token=${getAuthToken()}`,
PUBLIC_PICTIQUE_BASE_URL
).toString();
const eventSource = new EventSource(sseUrl);
eventSourceRef = eventSource;

eventSource.onopen = () => {
console.log('Successfully connected.');
};

eventSource.onmessage = (e) => {
try {
const data = JSON.parse(e.data);

// Handle initial payload with metadata vs real-time new messages
if (data.type === 'initial') {
// Initial batch from SSE connection
const transformed = transformMessages(data.messages);
messages = computeGroupFlags(removeDuplicateMessages(transformed));
hasMore = data.hasMore;
if (data.messages.length > 0) {
oldestMessageId = data.messages[0].id;
}
historyLoaded = true;
shouldScrollToBottom = true;
} else {
// Real-time new message(s) — could be array or wrapped
const rawMessages = Array.isArray(data) ? data : [data];
const transformed = transformMessages(rawMessages);

const existingIds = new Set(messages.map((msg) => msg.id));
const uniqueNew = transformed.filter((msg) => !existingIds.has(msg.id));

if (uniqueNew.length > 0) {
const merged = removeDuplicateMessages([...messages, ...uniqueNew]);
messages = computeGroupFlags(merged);
shouldScrollToBottom = true;
}
}

markMessagesRead();
} catch (error) {
console.error('Error parsing SSE message:', error);
}
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing onerror handler leaves SSE failures silent.

If the SSE connection fails or drops, there's no error handling or retry logic. Users will see an infinite loading spinner with no feedback.

🛠️ Proposed fix: Add error handler with user feedback
 eventSource.onopen = () => {
   console.log('Successfully connected.');
 };

+eventSource.onerror = (err) => {
+  console.error('SSE connection error:', err);
+  // Optionally set an error state to display to the user
+  // or implement reconnection logic
+};
+
 eventSource.onmessage = (e) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function watchEventStream() {
const sseUrl = new URL(
`/api/chats/${id}/events?token=${getAuthToken()}`,
PUBLIC_PICTIQUE_BASE_URL
).toString();
const eventSource = new EventSource(sseUrl);
eventSourceRef = eventSource;
eventSource.onopen = () => {
console.log('Successfully connected.');
};
eventSource.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
// Handle initial payload with metadata vs real-time new messages
if (data.type === 'initial') {
// Initial batch from SSE connection
const transformed = transformMessages(data.messages);
messages = computeGroupFlags(removeDuplicateMessages(transformed));
hasMore = data.hasMore;
if (data.messages.length > 0) {
oldestMessageId = data.messages[0].id;
}
historyLoaded = true;
shouldScrollToBottom = true;
} else {
// Real-time new message(s) — could be array or wrapped
const rawMessages = Array.isArray(data) ? data : [data];
const transformed = transformMessages(rawMessages);
const existingIds = new Set(messages.map((msg) => msg.id));
const uniqueNew = transformed.filter((msg) => !existingIds.has(msg.id));
if (uniqueNew.length > 0) {
const merged = removeDuplicateMessages([...messages, ...uniqueNew]);
messages = computeGroupFlags(merged);
shouldScrollToBottom = true;
}
}
markMessagesRead();
} catch (error) {
console.error('Error parsing SSE message:', error);
}
};
}
async function watchEventStream() {
const sseUrl = new URL(
`/api/chats/${id}/events?token=${getAuthToken()}`,
PUBLIC_PICTIQUE_BASE_URL
).toString();
const eventSource = new EventSource(sseUrl);
eventSourceRef = eventSource;
eventSource.onopen = () => {
console.log('Successfully connected.');
};
eventSource.onerror = (err) => {
console.error('SSE connection error:', err);
// Optionally set an error state to display to the user
// or implement reconnection logic
};
eventSource.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
// Handle initial payload with metadata vs real-time new messages
if (data.type === 'initial') {
// Initial batch from SSE connection
const transformed = transformMessages(data.messages);
messages = computeGroupFlags(removeDuplicateMessages(transformed));
hasMore = data.hasMore;
if (data.messages.length > 0) {
oldestMessageId = data.messages[0].id;
}
historyLoaded = true;
shouldScrollToBottom = true;
} else {
// Real-time new message(s) — could be array or wrapped
const rawMessages = Array.isArray(data) ? data : [data];
const transformed = transformMessages(rawMessages);
const existingIds = new Set(messages.map((msg) => msg.id));
const uniqueNew = transformed.filter((msg) => !existingIds.has(msg.id));
if (uniqueNew.length > 0) {
const merged = removeDuplicateMessages([...messages, ...uniqueNew]);
messages = computeGroupFlags(merged);
shouldScrollToBottom = true;
}
}
markMessagesRead();
} catch (error) {
console.error('Error parsing SSE message:', error);
}
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/pictique/client/src/routes/`(protected)/messages/[id]/+page.svelte
around lines 127 - 174, The SSE connection in watchEventStream currently lacks
an onerror handler so failures are silent; add eventSource.onerror to log the
error, set a local connection state (e.g., connectionError or sseError) so the
UI can stop any loading spinner, and gracefully close/cleanup eventSourceRef on
error; implement simple retry logic that closes the current EventSource, waits
with exponential backoff (or fixed delay) and re-invokes watchEventStream to
reconnect (ensure you clear previous handlers to avoid leaks), and call
markMessagesRead/update historyLoaded or other UI flags as appropriate so users
see feedback when the stream drops.

Comment on lines +176 to +181
async function handleSend() {
await apiClient.post(`/api/chats/${id}/messages`, {
text: messageValue
});
messageValue = '';
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider adding error handling for message send failures.

If the POST fails, messageValue is still cleared but the user receives no feedback that their message wasn't sent.

🛠️ Proposed improvement
 async function handleSend() {
-  await apiClient.post(`/api/chats/${id}/messages`, {
-    text: messageValue
-  });
-  messageValue = '';
+  try {
+    await apiClient.post(`/api/chats/${id}/messages`, {
+      text: messageValue
+    });
+    messageValue = '';
+  } catch (err) {
+    console.error('Failed to send message:', err);
+    // Optionally show user feedback
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function handleSend() {
await apiClient.post(`/api/chats/${id}/messages`, {
text: messageValue
});
messageValue = '';
}
async function handleSend() {
try {
await apiClient.post(`/api/chats/${id}/messages`, {
text: messageValue
});
messageValue = '';
} catch (err) {
console.error('Failed to send message:', err);
// Optionally show user feedback
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platforms/pictique/client/src/routes/`(protected)/messages/[id]/+page.svelte
around lines 176 - 181, The handleSend function clears messageValue regardless
of POST success and gives no user feedback on failure; wrap the apiClient.post
call in a try/catch, introduce a sending state (e.g., isSending) to prevent
duplicate sends, only clear messageValue after a successful response, and
surface errors to the user (e.g., set an errorMessage state or call the app's
toast/notification helper) and re-enable input on failure; reference the
handleSend function, apiClient.post(`/api/chats/${id}/messages`), and
messageValue when making these changes.

@coodos coodos merged commit 54df1dc into main Mar 15, 2026
4 checks passed
@coodos coodos deleted the feat/blabsy-and-pictique-infinite-chat-scroll branch March 15, 2026 21:02
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.

2 participants