Skip to content

Commit 8052bd7

Browse files
fix(giphy): allow using actions in quoted messages (#2849)
### 🎯 Goal This PR fixes an issue which made it impossible to use `/giphy` command in quoted messages. Ref: https://getstream.slack.com/archives/C02R5UCGN6N/p1759317325637579
1 parent 724dea6 commit 8052bd7

File tree

3 files changed

+44
-6
lines changed

3 files changed

+44
-6
lines changed

src/components/Message/QuotedMessage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useTranslationContext } from '../../context/TranslationContext';
1212
import { useChannelActionContext } from '../../context/ChannelActionContext';
1313
import { renderText as defaultRenderText } from './renderText';
1414
import type { MessageContextValue } from '../../context/MessageContext';
15+
import { useActionHandler } from './';
1516

1617
export type QuotedMessageProps = Pick<MessageContextValue, 'renderText'>;
1718

@@ -26,6 +27,7 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp
2627
} = useMessageContext('QuotedMessage');
2728
const { t, userLanguage } = useTranslationContext('QuotedMessage');
2829
const { jumpToMessage } = useChannelActionContext('QuotedMessage');
30+
const actionHandler = useActionHandler(message);
2931

3032
const renderText = propsRenderText ?? contextRenderText ?? defaultRenderText;
3133

@@ -96,7 +98,7 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp
9698
</div>
9799
</div>
98100
{message.attachments?.length ? (
99-
<Attachment attachments={message.attachments} />
101+
<Attachment actionHandler={actionHandler} attachments={message.attachments} />
100102
) : null}
101103
</>
102104
);

src/components/Message/hooks/useActionHandler.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useChannelStateContext } from '../../../context/ChannelStateContext';
33

44
import type React from 'react';
55
import type { LocalMessage } from 'stream-chat';
6+
import { useStableCallback } from '../../../utils/useStableCallback';
67

78
export type FormData = Record<string, string>;
89

@@ -19,15 +20,15 @@ export function useActionHandler(message?: LocalMessage): ActionHandlerReturnTyp
1920
const { removeMessage, updateMessage } = useChannelActionContext('useActionHandler');
2021
const { channel } = useChannelStateContext('useActionHandler');
2122

22-
return async (dataOrName, value, event) => {
23+
return useStableCallback(async (dataOrName, value, event) => {
2324
if (event) event.preventDefault();
2425

2526
if (!message || !updateMessage || !removeMessage || !channel) {
2627
console.warn(handleActionWarning);
2728
return;
2829
}
2930

30-
const messageID = message.id;
31+
const messageId = message.id;
3132
let formData: FormData = {};
3233

3334
// deprecated: value&name should be removed in favor of data obj
@@ -37,14 +38,14 @@ export function useActionHandler(message?: LocalMessage): ActionHandlerReturnTyp
3738
formData = { ...dataOrName };
3839
}
3940

40-
if (messageID) {
41-
const data = await channel.sendAction(messageID, formData);
41+
if (messageId) {
42+
const data = await channel.sendAction(messageId, formData);
4243

4344
if (data?.message) {
4445
updateMessage(data.message);
4546
} else {
4647
removeMessage(message);
4748
}
4849
}
49-
};
50+
});
5051
}

src/utils/useStableCallback.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useCallback, useRef } from 'react';
2+
3+
export type StableCallback<A extends unknown[], R> = (...args: A) => R;
4+
5+
/**
6+
* A utility hook implementing a stable callback. It takes in an unstable method that
7+
* is supposed to be invoked somewhere deeper in the DOM tree without making it
8+
* change its reference every time the parent component rerenders. It will also return
9+
* the value of the callback if it does return one.
10+
* A common use-case would be having a function whose invocation depends on state
11+
* somewhere high up in the DOM tree and wanting to use the same function deeper
12+
* down, for example in a leaf node and simply using useCallback results in
13+
* cascading dependency hell. If we wrap it in useStableCallback, we would be able
14+
* to:
15+
* - Use the same function as a dependency of another hook (since it is stable)
16+
* - Still invoke it and get the latest state
17+
*
18+
* **Caveats:**
19+
* - Never wrap a function that is supposed to return a React.ReactElement in
20+
* useStableCallback, since React will not know that the DOM needs to be updated
21+
* whenever the callback value changes (for example, renderItem from FlatList must
22+
* never be wrapped in this hook)
23+
* - Always prefer using a standard useCallback/stable function wherever possible
24+
* (the purpose of useStableCallback is to bridge the gap between top level contexts
25+
* and cascading rereders in downstream components - **not** as an escape hatch)
26+
* @param callback - the callback we want to stabilize
27+
*/
28+
export const useStableCallback = <A extends unknown[], R>(
29+
callback: StableCallback<A, R>,
30+
): StableCallback<A, R> => {
31+
const ref = useRef(callback);
32+
ref.current = callback;
33+
34+
return useCallback<StableCallback<A, R>>((...args) => ref.current(...args), []);
35+
};

0 commit comments

Comments
 (0)