From 7d89ce14d04231ba08b628e44786f676361a0080 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 2 Jun 2026 21:15:53 +0200 Subject: [PATCH] feat(messenger): send typing status + show 'is typing' indicator --- public/configuration/UITexts.example | 3 +- .../views/messenger/FriendsMessengerView.tsx | 66 ++++++++++++++++++- src/css/friends/FriendsView.css | 7 ++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/public/configuration/UITexts.example b/public/configuration/UITexts.example index 7a6fc2b..8c2aba2 100644 --- a/public/configuration/UITexts.example +++ b/public/configuration/UITexts.example @@ -266,5 +266,6 @@ "loading.task.rooms": "loading rooms...", "loading.task.engine": "loading graphics engine...", "catalog.gift_wrapping.gift_sent": "Done!", - "messenger.offline.delivered": "Sent while you were offline" + "messenger.offline.delivered": "Sent while you were offline", + "messenger.typing": "%FRIEND_NAME% is typing..." } diff --git a/src/components/friends/views/messenger/FriendsMessengerView.tsx b/src/components/friends/views/messenger/FriendsMessengerView.tsx index 0eaf484..b5fdd94 100644 --- a/src/components/friends/views/messenger/FriendsMessengerView.tsx +++ b/src/components/friends/views/messenger/FriendsMessengerView.tsx @@ -11,10 +11,52 @@ export const FriendsMessengerView: FC<{}> = props => const [ isVisible, setIsVisible ] = useState(false); const [ lastThreadId, setLastThreadId ] = useState(-1); const [ messageText, setMessageText ] = useState(''); - const { visibleThreads = [], activeThread = null, getMessageThread = null, sendMessage = null, setActiveThreadId = null, closeThread = null } = useMessenger(); + const { visibleThreads = [], activeThread = null, getMessageThread = null, sendMessage = null, setActiveThreadId = null, closeThread = null, typingUserIds = [], sendTypingStatus = null } = useMessenger(); const { report = null } = useHelp(); const { settings, translateOutgoing } = useTranslation(); const messagesBox = useRef(null); + const isTypingRef = useRef(false); + const typingTimeoutRef = useRef>(null); + + const stopTyping = () => + { + if(typingTimeoutRef.current) + { + clearTimeout(typingTimeoutRef.current); + typingTimeoutRef.current = null; + } + + if(isTypingRef.current && activeThread && activeThread.participant && (activeThread.participant.id > 0)) + { + sendTypingStatus(activeThread.participant.id, false); + } + + isTypingRef.current = false; + }; + + const handleInputChange = (value: string) => + { + setMessageText(value); + + const peerId = (activeThread && activeThread.participant) ? activeThread.participant.id : 0; + + if(peerId <= 0) return; + + if(!value.length) + { + stopTyping(); + return; + } + + if(!isTypingRef.current) + { + sendTypingStatus(peerId, true); + isTypingRef.current = true; + } + + if(typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); + typingTimeoutRef.current = setTimeout(() => stopTyping(), 4000); + }; const followFriend = () => (activeThread && activeThread.participant && SendMessageComposer(new FollowFriendMessageComposer(activeThread.participant.id))); const openProfile = () => (activeThread && activeThread.participant && GetUserProfile(activeThread.participant.id)); @@ -23,6 +65,8 @@ export const FriendsMessengerView: FC<{}> = props => { if(!activeThread || !messageText.length) return; + stopTyping(); + const trimmedText = messageText.trimStart(); const shouldTranslateOutgoing = settings.enabled && !!trimmedText.length && (trimmedText.charAt(0) !== ':'); @@ -100,6 +144,19 @@ export const FriendsMessengerView: FC<{}> = props => messagesBox.current.scrollTop = messagesBox.current.scrollHeight; }, [ isVisible, activeThread ]); + useEffect(() => + { + return () => + { + if(typingTimeoutRef.current) + { + clearTimeout(typingTimeoutRef.current); + typingTimeoutRef.current = null; + } + isTypingRef.current = false; + }; + }, [ activeThread ]); + useEffect(() => { if(isVisible && !activeThread) @@ -172,8 +229,13 @@ export const FriendsMessengerView: FC<{}> = props => + { activeThread.participant && (activeThread.participant.id > 0) && (typingUserIds.indexOf(activeThread.participant.id) >= 0) && +
+ { LocalizeText('messenger.typing', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) } +
} +
- setMessageText(event.target.value) } onKeyDown={ onKeyDown } /> + handleInputChange(event.target.value) } onKeyDown={ onKeyDown } /> diff --git a/src/css/friends/FriendsView.css b/src/css/friends/FriendsView.css index 34b9544..7383616 100644 --- a/src/css/friends/FriendsView.css +++ b/src/css/friends/FriendsView.css @@ -914,3 +914,10 @@ color: #4fc3f7; opacity: 1; } + +.messenger-typing-indicator { + padding: 2px 8px; + font-size: 11px; + font-style: italic; + opacity: 0.7; +}