-
Notifications
You must be signed in to change notification settings - Fork 298
fix(MessageComposer): prevent draft creation/clearing on disconnected channel #3249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -79,6 +79,10 @@ export type MessageComposerProps = { | |||||||||||||||||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| shouldSubmit?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => boolean; | ||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * When set to `true` disables clearing established state of the MessageComposerController upon component unmount. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| preventClearingOnUnmount?: boolean; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const MessageComposerProvider = (props: PropsWithChildren<MessageComposerProps>) => { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -95,9 +99,17 @@ const MessageComposerProvider = (props: PropsWithChildren<MessageComposerProps>) | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| useEffect( | ||||||||||||||||||||||||||||||||||||||
| () => () => { | ||||||||||||||||||||||||||||||||||||||
| messageComposer.createDraft().finally(() => messageComposer.clear()); | ||||||||||||||||||||||||||||||||||||||
| if (messageComposer.channel.disconnected) return; | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win Guard a missing channel before dereferencing it. The same provider checks Proposed fix- if (messageComposer.channel.disconnected) return;
+ if (!messageComposer.channel || messageComposer.channel.disconnected) return;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const promise = messageComposer.config.drafts.enabled | ||||||||||||||||||||||||||||||||||||||
| ? messageComposer.createDraft() | ||||||||||||||||||||||||||||||||||||||
| : Promise.resolve(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (props.preventClearingOnUnmount) return; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| promise.finally(() => messageComposer.clear()); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+104
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== find file =="
fd -a 'MessageComposer\.tsx$' . || true
echo "== outline =="
ast-grep outline src/components/MessageComposer/MessageComposer.tsx --view expanded || true
echo "== relevant lines =="
cat -n src/components/MessageComposer/MessageComposer.tsx | sed -n '70,130p'
echo "== search createDraft implementations/usages =="
rg -n "createDraft|preventClearingOnUnmount|clear\(" src -SRepository: GetStream/stream-chat-react Length of output: 5837 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate message composer controller files =="
rg -n "createDraft|useMessageComposerController|MessageComposerController|createDraft" src -S | head -200
echo "== files likely implementing controller =="
rg -l "createDraft|useMessageComposerController|MessageComposerController" src -SRepository: GetStream/stream-chat-react Length of output: 17001 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== package references for stream-chat dependency specs =="
for f in package.json yarn.lock package-lock.json pnpm-lock.yaml; do
[ -f "$f" ] && { echo "--- $f"; rg -n "(^stream-chat:|stream-chat@|/stream-chat@|stream-chat-react )" "$f" | head -80; }
done
echo "== inspect MessageComposer hook implementation =="
cat -n src/components/MessageComposer/hooks/useMessageComposerController.tsRepository: GetStream/stream-chat-react Length of output: 240 🌐 Web query:
💡 Result: When using the Stream Chat Citations:
🌐 Web query:
💡 Result: In the Stream Chat JavaScript/React SDKs, the MessageComposer class acts as the central orchestrator for message composition, including the management of drafts [1][2]. Regarding the createDraft and fetch (getDraft) functionality within the MessageComposer: 1. Create Draft: You can create a draft using the composer's createDraft method [1][3]. This method does not require parameters as it automatically extracts data from the composer's internal state and configured draft composition middleware [1][3]. It typically performs an asynchronous operation to save the draft to the server [1][3]. 2. Fetch (Get) Draft: You can retrieve a draft using the composer's getDraft method [1][3]. This method initiates the process of fetching an existing draft for the channel or thread associated with the composer instance and initializes the composer's state with the retrieved data [4]. Internally, it may first attempt to retrieve a draft from a local database (if configured) before fetching the latest version from the server via the underlying channel's getDraft method [4]. Key Implementation Details: - Enabling Drafts: Drafts must be enabled in the MessageComposer configuration (e.g., config: { drafts: { enabled: true } }) [1][5]. - Automatic State Handling: The MessageComposer is designed to automatically handle local state updates in response to draft-related events (such as draft.updated or draft.deleted), reducing the need for manual synchronization in most use cases [1][3]. - Architecture: The MessageComposer uses middleware systems to handle various aspects of composition, including drafts [6][7]. The core logic is centralized within the MessageComposer class, which coordinates with the channel or thread context [1][2]. For developers building custom UI, the MessageComposer instance can be accessed via the useMessageComposer hook in React, which provides access to these draft management methods [8][9]. Citations:
🌐 Web query:
💡 Result: In the Stream Chat JavaScript SDK, you can create a draft message for a channel or thread using the Citations:
🌐 Web query:
💡 Result: In the Stream Chat SDK, draft management is handled differently depending on whether you are using the higher-level MessageComposer UI component or the lower-level Channel API. MessageComposer (UI SDKs) The MessageComposer component provides a high-level API to manage drafts within your UI [1][2]. You can enable draft functionality through the MessageComposer configuration [1][3]. The MessageComposer exposes the following methods to manage these drafts directly [1][4]: await composer.createDraft; await composer.deleteDraft; await composer.getDraft; These methods do not require parameters, as the MessageComposer automatically extracts the necessary data from its internal state and draft composition middleware [1][3]. It also automatically handles local state updates in response to draft.updated and draft.deleted events [1][4]. Channel API (Core JS SDK) If you are interacting directly with the Channel object, you use the createDraft method provided by the channel instance [5][6]. This method creates or updates a draft message for a channel or a thread [5][7]. To create or update a draft using the channel object, pass a payload containing the message details [5][6]: await channel.createDraft({ text: "this is a draft message", parent_id: parentMessageId // optional, for threads }); Key points regarding the Channel API: - Only one draft per channel or thread can exist at a time; creating a new one overrides the existing draft [5][7]. - The method includes logic to handle offline support, queuing the task if it fails due to connectivity issues [6][8]. - You can also manage drafts using channel.getDraft and channel.deleteDraft [6][8]. Citations:
🌐 Web query:
💡 Result: There is no method named sendDraft in the Stream Chat JavaScript SDK. The draft functionality in stream-chat-js is managed through creation, retrieval, and deletion methods on the channel object, and there is no direct "send" operation for a draft [1][2][3]. To handle draft messages, you typically use the following methods on a channel instance: 1. Create or Update a Draft: Use channel.createDraft(message). This creates or updates a draft message for that channel (or thread, if a parent_id is provided) [1][3]. 2. Retrieve a Draft: Use channel.getDraft({ parent_id }). This fetches the current draft for the channel or specified thread [1][3]. 3. Delete a Draft: Use channel.deleteDraft({ parent_id }). This removes the draft from the channel or thread [3]. If you are using the Stream Chat React SDK, the MessageComposer component provides a high-level API for managing drafts, such as composer.createDraft, composer.deleteDraft, and composer.getDraft [4][5]. The composer can also call a compose or composeDraft method to process the message, but it does not use a method named sendDraft [4][5]. To send a draft, you would typically retrieve the draft content and then call the standard channel.sendMessage or composer.compose methods [1][4][6]. Citations:
🌐 Web query:
💡 Result: In the Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== fetch stream-chat 9.38.0 MessageComposer createDraft source =="
python3 - <<'PY'
import json, urllib.request
url="https://cdn.jsdelivr.net/npm/stream-chat@9.38.0/src/messageComposer/messageComposer.ts"
try:
resp=urllib.request.urlopen(url, timeout=20)
data=resp.read().decode("utf-8")
lines=data.splitlines()
for i,l in enumerate(lines,start=1):
if "createDraft" in l:
start=max(1,i-15); end=min(len(lines),i+80)
print(f"--- {i} ---")
for n in range(start,end+1):
print(f"{n:5}: {lines[n-1]}")
print()
except Exception as e:
print(f"ERROR: {e}")
exit(2)
PY
echo "== fetch stream-chat 9.38.0 channel.ts draft createDraft source =="
python3 - <<'PY'
import urllib.request
url="https://cdn.jsdelivr.net/npm/stream-chat@9.38.0/src/channel.ts"
try:
resp=urllib.request.urlopen(url, timeout=20)
data=resp.read().decode("utf-8")
lines=data.splitlines()
for i,l in enumerate(lines,start=1):
if "createDraft" in l or "deleteDraft" in l or "getDraft" in l:
start=max(1,i-5); end=min(len(lines),i+45)
print(f"--- {i} ---")
for n in range(start,end+1):
s=lines[n-1]
if n==i or "offlineDb" in s or "shouldQueueTask" in s or "executeTask" in s or "channel.update" in s or "return" in s or "catch" in s or "try" in s:
print(f"{n:5}: {s}")
print()
except Exception as e:
print(f"ERROR: {e}")
exit(2)
PYRepository: GetStream/stream-chat-react Length of output: 373 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import ssl, urllib.request, urllib.error
ctx=ssl._create_unverified_context() if hasattr(ssl, '_create_unverified_context') else None
for url in [
"https://cdn.jsdelivr.net/npm/stream-chat@9.38.0/src/messageComposer/messageComposer.ts",
"https://cdn.jsdelivr.net/npm/stream-chat@9.38.0/src/channel.ts",
]:
try:
req=urllib.request.Request(url, headers={"User-Agent":"curl"})
data=urllib.request.urlopen(req, context=ctx, timeout=20).read().decode("utf-8")
lines=data.splitlines()
print(f"--- SOURCE {url}")
for i,l in enumerate(lines,start=1):
if "createDraft" in l or "deleteDraft" in l or "getDraft" in l:
start=max(1,i-8); end=min(len(lines),i+55)
print(f"--- TOKEN {i} ---")
for n in range(start,end+1):
s=lines[n-1]
if n==i or any(k in s for k in ["offlineDb","shouldQueueTask","executeTask","channel.update","return","catch","try","composeDraft","draftId","draft"]):
print(f"{n:5}: {s}")
print()
except urllib.error.HTTPError as e:
print(f"HTTP {e.code} {url}")
except Exception as e:
print(f"ERROR {url}: {e}")
PYRepository: GetStream/stream-chat-react Length of output: 28311 Handle rejected draft creation during cleanup. If the channel disconnects after Proposed fix- if (props.preventClearingOnUnmount) return;
- promise.finally(() => messageComposer.clear());
+ if (props.preventClearingOnUnmount) {
+ void promise.catch(console.error);
+ return;
+ }
+ void promise
+ .catch(console.error)
+ .finally(() => messageComposer.clear());📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| [messageComposer], | ||||||||||||||||||||||||||||||||||||||
| [messageComposer, props.preventClearingOnUnmount], | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+108
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Locate MessageComposer files"
fd -a 'MessageComposer\.tsx$' . || true
echo
echo "Relevant snippet and nearby effect code"
if [ -f src/components/MessageComposer/MessageComposer.tsx ]; then
wc -l src/components/MessageComposer/MessageComposer.tsx
sed -n '1,170p' src/components/MessageComposer/MessageComposer.tsx | cat -n
fiRepository: GetStream/stream-chat-react Length of output: 8005 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Inspect controller draft/create/clear definitions"
rg -n "createDraft|clear\\(|drafts|state" src/components/MessageComposer -S
echo
echo "Inspect controller hooks"
fd -a 'Hooks?|Controller|.*Controller.*|.*Controller.*' src/components/MessageComposer | sed 's#^`#/`#' | head -50
echo
fd -a 'Hooks?|.*Controller.*' src/components/MessageComposer | while read -r f; do
echo "--- $f"
wc -l "$f"
sed -n '1,260p' "$f" | cat -n
done
echo
echo "Check package React version type/imports are safe for useRef"
if [ -f package.json ]; then
node -e "const p=require('./package.json'); console.log(JSON.stringify({react:p.dependencies?.react ?? p.devDependencies?.react}, null, 2))"
fi
rg "react|react-dom" package.json yarn.lock 2>/dev/null | head -40 || trueRepository: GetStream/stream-chat-react Length of output: 29499 Avoid running unmount cleanup while re-running this effect.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Document the new public prop in the guide page.
This changes the public
MessageComposerPropsAPI. AddpreventClearingOnUnmountand its unmount semantics to the affected MessageComposer props guide; the inline comment alone is insufficient.As per coding guidelines, public API changes must update inline docs and affected guide pages.
🤖 Prompt for AI Agents
Source: Coding guidelines