feat(messenger): send typing status + show 'is typing' indicator

This commit is contained in:
simoleo89
2026-06-02 21:15:53 +02:00
parent 361ab27853
commit 7d89ce14d0
3 changed files with 73 additions and 3 deletions
+2 -1
View File
@@ -266,5 +266,6 @@
"loading.task.rooms": "loading rooms...", "loading.task.rooms": "loading rooms...",
"loading.task.engine": "loading graphics engine...", "loading.task.engine": "loading graphics engine...",
"catalog.gift_wrapping.gift_sent": "Done!", "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..."
} }
@@ -11,10 +11,52 @@ export const FriendsMessengerView: FC<{}> = props =>
const [ isVisible, setIsVisible ] = useState(false); const [ isVisible, setIsVisible ] = useState(false);
const [ lastThreadId, setLastThreadId ] = useState(-1); const [ lastThreadId, setLastThreadId ] = useState(-1);
const [ messageText, setMessageText ] = useState(''); 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 { report = null } = useHelp();
const { settings, translateOutgoing } = useTranslation(); const { settings, translateOutgoing } = useTranslation();
const messagesBox = useRef<HTMLDivElement>(null); const messagesBox = useRef<HTMLDivElement>(null);
const isTypingRef = useRef<boolean>(false);
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout>>(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 followFriend = () => (activeThread && activeThread.participant && SendMessageComposer(new FollowFriendMessageComposer(activeThread.participant.id)));
const openProfile = () => (activeThread && activeThread.participant && GetUserProfile(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; if(!activeThread || !messageText.length) return;
stopTyping();
const trimmedText = messageText.trimStart(); const trimmedText = messageText.trimStart();
const shouldTranslateOutgoing = settings.enabled && !!trimmedText.length && (trimmedText.charAt(0) !== ':'); const shouldTranslateOutgoing = settings.enabled && !!trimmedText.length && (trimmedText.charAt(0) !== ':');
@@ -100,6 +144,19 @@ export const FriendsMessengerView: FC<{}> = props =>
messagesBox.current.scrollTop = messagesBox.current.scrollHeight; messagesBox.current.scrollTop = messagesBox.current.scrollHeight;
}, [ isVisible, activeThread ]); }, [ isVisible, activeThread ]);
useEffect(() =>
{
return () =>
{
if(typingTimeoutRef.current)
{
clearTimeout(typingTimeoutRef.current);
typingTimeoutRef.current = null;
}
isTypingRef.current = false;
};
}, [ activeThread ]);
useEffect(() => useEffect(() =>
{ {
if(isVisible && !activeThread) if(isVisible && !activeThread)
@@ -172,8 +229,13 @@ export const FriendsMessengerView: FC<{}> = props =>
<FriendsMessengerThreadView thread={ activeThread } /> <FriendsMessengerThreadView thread={ activeThread } />
</div> </div>
{ activeThread.participant && (activeThread.participant.id > 0) && (typingUserIds.indexOf(activeThread.participant.id) >= 0) &&
<div className="messenger-typing-indicator">
{ LocalizeText('messenger.typing', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) }
</div> }
<div className="messenger-input-row"> <div className="messenger-input-row">
<input maxLength={ 255 } placeholder={ LocalizeText('messenger.window.input.default', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) } type="text" value={ messageText } onChange={ event => setMessageText(event.target.value) } onKeyDown={ onKeyDown } /> <input maxLength={ 255 } placeholder={ LocalizeText('messenger.window.input.default', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) } type="text" value={ messageText } onChange={ event => handleInputChange(event.target.value) } onKeyDown={ onKeyDown } />
<button className="messenger-btn send" onClick={ () => void send() }> <button className="messenger-btn send" onClick={ () => void send() }>
{ LocalizeText('widgets.chatinput.say') } { LocalizeText('widgets.chatinput.say') }
</button> </button>
+7
View File
@@ -914,3 +914,10 @@
color: #4fc3f7; color: #4fc3f7;
opacity: 1; opacity: 1;
} }
.messenger-typing-indicator {
padding: 2px 8px;
font-size: 11px;
font-style: italic;
opacity: 0.7;
}