Skip to content
48 changes: 23 additions & 25 deletions src/modules/GroupChannel/components/Message/MessageView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { EveryMessage, RenderCustomSeparatorProps, RenderMessageParamsType, ReplyType } from '../../../../types';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { type EmojiCategory, EmojiContainer, User } from '@sendbird/chat';
import { GroupChannel } from '@sendbird/chat/groupChannel';
import type { FileMessage, UserMessage, UserMessageCreateParams, UserMessageUpdateParams } from '@sendbird/chat/message';
Expand Down Expand Up @@ -189,7 +189,7 @@ const MessageView = (props: MessageViewProps) => {
const [showEdit, setShowEdit] = useState(false);
const [showRemove, setShowRemove] = useState(false);
const [showFileViewer, setShowFileViewer] = useState(false);
const [isAnimated, setIsAnimated] = useState(false);
// isAnimated state removed — animation now driven by animatedMessageId + onAnimationEnd
const [mentionNickname, setMentionNickname] = useState('');
const [mentionedUsers, setMentionedUsers] = useState<User[]>([]);
const [mentionedUserIds, setMentionedUserIds] = useState<string[]>([]);
Expand Down Expand Up @@ -245,29 +245,26 @@ const MessageView = (props: MessageViewProps) => {
if (usedInLegacy) handleScroll?.(true);
}, []);

useLayoutEffect(() => {
const timeouts: ReturnType<typeof setTimeout>[] = [];

if (animatedMessageId === message.messageId && messageScrollRef?.current) {
timeouts.push(
setTimeout(() => {
setIsAnimated(true);
}, 500),
);

timeouts.push(
setTimeout(() => {
setAnimatedMessageId(null);
onMessageAnimated?.();
}, 1600),
);
} else {
setIsAnimated(false);
// Animation: once triggered, protect with local state until CSS animation completes
const [showBounce, setShowBounce] = useState(false);
const isAnimationTarget = animatedMessageId === message.messageId;

useEffect(() => {
if (isAnimationTarget && !showBounce) {
setShowBounce(true);
}
}, [isAnimationTarget]);

const handleAnimationEnd = useCallback((e: React.AnimationEvent) => {
Comment thread
sf-tyler-jeong marked this conversation as resolved.
if (e.animationName === 'bounce') {
setShowBounce(false);
// Only clear if this message is still the animation target
if (animatedMessageId === message.messageId) {
setAnimatedMessageId(null);
onMessageAnimated?.();
}
}
return () => {
timeouts.forEach((it) => clearTimeout(it));
};
}, [animatedMessageId, messageScrollRef.current, message.messageId]);
}, [animatedMessageId, message.messageId, setAnimatedMessageId, onMessageAnimated]);

useLayoutEffect(() => {
if (newMessageIds?.length > 0 && newMessageIds.includes(message.messageId)) {
Expand Down Expand Up @@ -436,8 +433,9 @@ const MessageView = (props: MessageViewProps) => {
<div
className={classnames(
'sendbird-msg-hoc sendbird-msg--scroll-ref',
isAnimated && 'sendbird-msg-hoc__animated',
showBounce && 'sendbird-msg-hoc__animated',
)}
onAnimationEnd={showBounce ? handleAnimationEnd : undefined}
data-testid="sendbird-message-view"
style={children || renderMessage ? undefined : { marginBottom: '2px' }}
data-sb-message-id={message.messageId}
Expand Down
14 changes: 10 additions & 4 deletions src/modules/GroupChannel/context/GroupChannelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,17 +300,23 @@ const GroupChannelManager :React.FC<React.PropsWithChildren<GroupChannelProvider
};
}, [messageDataSource.initialized, state.currentChannel?.url]);

// Starting point handling
// Starting point handling — skip when animated message handles scroll
useEffect(() => {
if (typeof startingPoint === 'number' && state.initialized) {
if (typeof startingPoint === 'number' && state.initialized && !_animatedMessageId) {
actions.scrollToMessage(startingPoint, 0, false, false);
}
}, [state.initialized, startingPoint]);

// Animated message handling
// Animated message handling — scroll + animation
useEffect(() => {
if (_animatedMessageId) {
actions.setAnimatedMessageId(_animatedMessageId);
if (typeof startingPoint === 'number') {
// Search result click: scroll to message and animate
actions.scrollToMessage(startingPoint, _animatedMessageId, true, false);
} else {
Comment thread
sf-tyler-jeong marked this conversation as resolved.
Outdated
// Thread parent jump: scroll already handled by startingPoint effect, just animate
actions.setAnimatedMessageId(_animatedMessageId);
}
}
}, [_animatedMessageId]);

Expand Down
2 changes: 0 additions & 2 deletions src/modules/GroupChannel/context/hooks/useGroupChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@ export const useGroupChannel = () => {

clickHandler.deactivate();

setAnimatedMessageId(null);

const message = state.messages.find((it) => it.messageId === messageId || it.createdAt === createdAt);

if (message) {
Expand Down
Loading