diff --git a/packages/react/src/hooks/useRCAuth.js b/packages/react/src/hooks/useRCAuth.js index 83b013353b..d905be40c6 100644 --- a/packages/react/src/hooks/useRCAuth.js +++ b/packages/react/src/hooks/useRCAuth.js @@ -25,45 +25,67 @@ export const useRCAuth = () => { const handleLogin = async (userOrEmail, password, code) => { try { const res = await RCInstance.login(userOrEmail, password, code); + + // Handle specific error codes or generic Unauthorized if (res.error === 'Unauthorized' || res.error === 403) { dispatchToastMessage({ type: 'error', - message: - 'Invalid username or password. Please check your credentials and try again', + message: 'Invalid username or password. Please check your credentials.', + }); + return { status: 'error', error: 'Unauthorized' }; + } + + // Handle Two-Factor Authentication (TOTP) + if (res.error === 'totp-required') { + setPassword(password); + setEmailorUser(userOrEmail); + setIsLoginModalOpen(false); + setIsTotpModalOpen(true); + dispatchToastMessage({ + type: 'info', + message: 'MFA Required: Please enter the code from your authenticator app.', }); - } else { - if (res.error === 'totp-required') { - setPassword(password); - setEmailorUser(userOrEmail); - setIsLoginModalOpen(false); - setIsTotpModalOpen(true); - dispatchToastMessage({ - type: 'info', - message: 'Please Open your authentication app and enter the code.', - }); - } else if (res.error === 'totp-invalid') { - dispatchToastMessage({ - type: 'error', - message: 'Invalid TOTP Time-based One-time Password.', - }); - } + return { status: 'totp-required' }; + } + + if (res.error === 'totp-invalid') { + dispatchToastMessage({ + type: 'error', + message: 'Invalid TOTP code. Please try again.', + }); + return { status: 'error', error: 'totp-invalid' }; + } + + // Handle Successful Login + if (res.status === 'success' || (res.me && !res.error)) { + setIsLoginModalOpen(false); + setUserAvatarUrl(res.me.avatarUrl); + setAuthenticatedUserUsername(res.me.username); + setIsUserAuthenticated(true); + setIsTotpModalOpen(false); + + // Clear sensitive temporary data + setEmailorUser(null); + setPassword(null); + + dispatchToastMessage({ + type: 'success', + message: `Welcome back, ${res.me.username}!`, + }); + return { status: 'success', user: res.me }; + } - if (res.status === 'success') { - setIsLoginModalOpen(false); - setUserAvatarUrl(res.me.avatarUrl); - setAuthenticatedUserUsername(res.me.username); - setIsUserAuthenticated(true); - setIsTotpModalOpen(false); - setEmailorUser(null); - setPassword(null); - dispatchToastMessage({ - type: 'success', - message: 'Successfully logged in', - }); - } + // Catch-all for other response errors + if (res.error) { + throw new Error(res.reason || res.error); } } catch (e) { - console.error('A error occurred while setting up user', e); + console.error('Login implementation error:', e); + dispatchToastMessage({ + type: 'error', + message: 'Authentication failed due to a network or server error.', + }); + return { status: 'error', error: e.message }; } }; diff --git a/packages/react/src/hooks/useShowCommands.js b/packages/react/src/hooks/useShowCommands.js index 1379f267c4..4e10c98052 100644 --- a/packages/react/src/hooks/useShowCommands.js +++ b/packages/react/src/hooks/useShowCommands.js @@ -10,8 +10,9 @@ const useShowCommands = (commands, setFilteredCommands, setShowCommandList) => const tokens = e.target.value.slice(0, cursor).split(/\s+/); if (tokens.length === 1 && tokens[0].startsWith('/')) { - setFilteredCommands(getFilteredCommands(tokens[0])); - setShowCommandList(true); + const filtered = getFilteredCommands(tokens[0]); + setFilteredCommands(filtered); + setShowCommandList(filtered.length > 0); } else { setFilteredCommands([]); setShowCommandList(false); diff --git a/packages/react/src/views/ChannelState/ChannelState.js b/packages/react/src/views/ChannelState/ChannelState.js index f5262d3edb..90ef8c5347 100644 --- a/packages/react/src/views/ChannelState/ChannelState.js +++ b/packages/react/src/views/ChannelState/ChannelState.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Box, Icon, @@ -34,4 +35,12 @@ const ChannelState = ({ ); }; +ChannelState.propTypes = { + className: PropTypes.string, + style: PropTypes.object, + status: PropTypes.string, + iconName: PropTypes.string, + instructions: PropTypes.string, +}; + export default ChannelState; diff --git a/packages/react/src/views/ChatBody/ChatBody.js b/packages/react/src/views/ChatBody/ChatBody.js index 34f5c8bf40..b0f5dda15e 100644 --- a/packages/react/src/views/ChatBody/ChatBody.js +++ b/packages/react/src/views/ChatBody/ChatBody.js @@ -146,43 +146,46 @@ const ChatBody = ({ ); useEffect(() => { - RCInstance.auth.onAuthChange((user) => { - if (user) { - RCInstance.addMessageListener(addMessage); - RCInstance.addMessageDeleteListener(removeMessage); - RCInstance.addActionTriggeredListener(onActionTriggerResponse); - RCInstance.addUiInteractionListener(onActionTriggerResponse); - } - }); - - return () => { + const removeAllListeners = () => { RCInstance.removeMessageListener(addMessage); RCInstance.removeMessageDeleteListener(removeMessage); RCInstance.removeActionTriggeredListener(onActionTriggerResponse); RCInstance.removeUiInteractionListener(onActionTriggerResponse); }; - }, [RCInstance, addMessage, removeMessage, onActionTriggerResponse]); - useEffect(() => { - RCInstance.auth.onAuthChange((user) => { + const unsubscribe = RCInstance.auth.onAuthChange((user) => { if (user) { + // Clear old listeners before adding new ones to avoid duplicates + removeAllListeners(); + RCInstance.addMessageListener(addMessage); + RCInstance.addMessageDeleteListener(removeMessage); + RCInstance.addActionTriggeredListener(onActionTriggerResponse); + RCInstance.addUiInteractionListener(onActionTriggerResponse); + getMessagesAndRoles(); setHasMoreMessages(true); - } else { - getMessagesAndRoles(anonymousMode); - } - }); - }, [RCInstance, anonymousMode, getMessagesAndRoles]); - - useEffect(() => { - RCInstance.auth.onAuthChange((user) => { - if (user) { fetchAndSetPermissions(); } else { + removeAllListeners(); + getMessagesAndRoles(anonymousMode); permissionsRef.current = null; } }); - }, []); + + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + removeAllListeners(); + }; + }, [ + RCInstance, + addMessage, + removeMessage, + onActionTriggerResponse, + anonymousMode, + getMessagesAndRoles, + fetchAndSetPermissions, + permissionsRef, + ]); // Expose clearUnreadDivider function via ref for ChatInput to call useEffect(() => { @@ -309,9 +312,15 @@ const ChatBody = ({ useEffect(() => { if (messageListRef.current) { - messageListRef.current.scrollTop = messageListRef.current.scrollHeight; + const { scrollTop, scrollHeight, clientHeight } = messageListRef.current; + const isAtBottom = scrollHeight - scrollTop - clientHeight < 100; + const isInitialLoad = messages.length > 0 && scrollTop === 0; + + if (isAtBottom || isInitialLoad) { + messageListRef.current.scrollTop = scrollHeight; + } } - }, [messages]); + }, [messages, messageListRef]); useEffect(() => { checkOverflow(); @@ -429,7 +438,7 @@ const ChatBody = ({ {uiKitModalOpen && ( - + )} @@ -449,4 +458,10 @@ export default ChatBody; ChatBody.propTypes = { anonymousMode: PropTypes.bool, showRoles: PropTypes.bool, + messageListRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), + scrollToBottom: PropTypes.func, + clearUnreadDividerRef: PropTypes.shape({ current: PropTypes.func }), }; diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 9143598d30..9af6db784f 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -428,7 +428,12 @@ const ChatHeader = ({ {avatarUrl && ( - + )} {surfaceOptions.length > 0 && ( diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index e753b689ae..c87de628b0 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -144,7 +144,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { ); useEffect(() => { - RCInstance.auth.onAuthChange((user) => { + const unsubscribe = RCInstance.auth.onAuthChange((user) => { if (user) { RCInstance.getCommandsList() .then((data) => setCommands(data.commands || [])) @@ -157,6 +157,11 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { .catch(console.error); } }); + + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + if (timerRef.current) clearTimeout(timerRef.current); + }; }, [RCInstance, isChannelPrivate, setMembersHandler]); useEffect(() => { @@ -258,13 +263,14 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { return; } if (messageRef.current.value?.length) { + if (timerRef.current) clearTimeout(timerRef.current); typingRef.current = true; timerRef.current = setTimeout(() => { typingRef.current = false; - }, [15000]); + }, 15000); await RCInstance.sendTypingStatus(username, true); } else { - clearTimeout(timerRef.current); + if (timerRef.current) clearTimeout(timerRef.current); typingRef.current = false; await RCInstance.sendTypingStatus(username, false); } @@ -275,6 +281,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const sendTypingStop = async () => { try { + if (timerRef.current) clearTimeout(timerRef.current); typingRef.current = false; await RCInstance.sendTypingStatus(username, false); } catch (e) { diff --git a/packages/react/src/views/CommandList/CommandsList.js b/packages/react/src/views/CommandList/CommandsList.js index ad4d9f1d45..7afa4264a7 100644 --- a/packages/react/src/views/CommandList/CommandsList.js +++ b/packages/react/src/views/CommandList/CommandsList.js @@ -30,6 +30,8 @@ function CommandsList({ const handleCommandClick = useCallback( async (command) => { + if (!command) return; + if (!command) return; const commandName = command.command; const currentMessage = messageRef.current.value; const tokens = (currentMessage || '').split(' '); diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index f3b94c7b48..73b434d22a 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -18,7 +18,7 @@ import { import { ChatLayout } from './ChatLayout'; import { ChatHeader } from './ChatHeader'; import { RCInstanceProvider } from '../context/RCInstance'; -import { useUserStore, useLoginStore, useMessageStore } from '../store'; +import { useUserStore, useLoginStore } from '../store'; import DefaultTheme from '../theme/DefaultTheme'; import { getTokenStorage } from '../lib/auth'; import { styles } from './EmbeddedChat.styles'; @@ -134,7 +134,7 @@ const EmbeddedChat = (props) => { }, [RCInstance, auth, setIsLoginIn]); useEffect(() => { - RCInstance.auth.onAuthChange((user) => { + const unsubscribe = RCInstance.auth.onAuthChange((user) => { if (user) { RCInstance.connect() .then(() => { @@ -150,8 +150,17 @@ const EmbeddedChat = (props) => { .catch(console.error); } else { setIsUserAuthenticated(false); + setAuthenticatedAvatarUrl(''); + setAuthenticatedUsername(''); + setAuthenticatedUserId(''); + setAuthenticatedName(''); + setAuthenticatedUserRoles([]); } }); + + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + }; }, [ RCInstance, setAuthenticatedName, diff --git a/packages/react/src/views/EmojiPicker/EmojiPicker.js b/packages/react/src/views/EmojiPicker/EmojiPicker.js index 6501eb8320..92557b4b8b 100644 --- a/packages/react/src/views/EmojiPicker/EmojiPicker.js +++ b/packages/react/src/views/EmojiPicker/EmojiPicker.js @@ -18,7 +18,6 @@ const CustomEmojiPicker = ({ const theme = useTheme(); const styles = getEmojiPickerStyles(theme); const previewConfig = { - defaultEmoji: '1f60d', defaultCaption: 'None', showPreview: true, }; diff --git a/packages/react/src/views/LoginForm/LoginForm.js b/packages/react/src/views/LoginForm/LoginForm.js index 1285e5e1eb..9c3f945c92 100644 --- a/packages/react/src/views/LoginForm/LoginForm.js +++ b/packages/react/src/views/LoginForm/LoginForm.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; import { css } from '@emotion/react'; import { GenericModal, @@ -12,7 +13,7 @@ import { useLoginStore } from '../../store'; import { useRCAuth } from '../../hooks/useRCAuth'; import styles from './LoginForm.styles'; -export default function LoginForm() { +const LoginForm = () => { const [userOrEmail, setUserOrEmail] = useState(null); const [password, setPassword] = useState(null); const [showPassword, setShowPassword] = useState(false); @@ -108,6 +109,7 @@ export default function LoginForm() { /> {field.label === 'Password' && ( > ) : null; -} +}; + +LoginForm.propTypes = {}; // No props, but good to have for consistency + +export default LoginForm; diff --git a/packages/react/src/views/Message/MessageToolbox.js b/packages/react/src/views/Message/MessageToolbox.js index 75bdc7467d..d24181144f 100644 --- a/packages/react/src/views/Message/MessageToolbox.js +++ b/packages/react/src/views/Message/MessageToolbox.js @@ -1,4 +1,4 @@ -import React, { useState, useContext, useMemo } from 'react'; +import React, { useState, useContext, useMemo } from "react"; import { Box, Modal, @@ -8,16 +8,16 @@ import { useComponentOverrides, appendClassNames, useTheme, -} from '@embeddedchat/ui-elements'; -import RCContext from '../../context/RCInstance'; -import { EmojiPicker } from '../EmojiPicker'; -import { getMessageToolboxStyles } from './Message.styles'; -import SurfaceMenu from '../SurfaceMenu/SurfaceMenu'; -import { Markdown } from '../Markdown'; -import Attachment from '../AttachmentHandler/Attachment'; +} from "@embeddedchat/ui-elements"; +import RCContext from "../../context/RCInstance"; +import { EmojiPicker } from "../EmojiPicker"; +import { getMessageToolboxStyles } from "./Message.styles"; +import SurfaceMenu from "../SurfaceMenu/SurfaceMenu"; +import { Markdown } from "../Markdown"; +import Attachment from "../AttachmentHandler/Attachment"; export const MessageToolbox = ({ - className = '', + className = "", message, variantStyles = {}, style = {}, @@ -42,16 +42,16 @@ export const MessageToolbox = ({ isEditing = false, optionConfig = { surfaceItems: [ - 'reaction', - 'reply', - 'quote', - 'star', - 'copy', - 'link', - 'pin', - 'edit', - 'delete', - 'report', + "reaction", + "reply", + "quote", + "star", + "copy", + "link", + "pin", + "edit", + "delete", + "report", ], menuItems: [], @@ -60,9 +60,9 @@ export const MessageToolbox = ({ ...props }) => { const { styleOverrides, classNames, configOverrides } = useComponentOverrides( - 'MessageToolbox', + "MessageToolbox", className, - style + style, ); const { RCInstance } = useContext(RCContext); const instanceHost = RCInstance.getHost(); @@ -81,121 +81,148 @@ export const MessageToolbox = ({ setShowDeleteModal(false); }; - const isAllowedToPin = userRoles.some((role) => pinRoles.has(role)); + const { + isAllowedToPin, + isAllowedToReport, + isAllowedToEditMessage, + isAllowedToDeleteMessage, + isAllowedToDeleteOwnMessage, + isAllowedToForceDeleteMessage, + isVisibleForMessageType, + canDeleteMessage, + } = useMemo(() => { + const isOwner = message.u._id === authenticatedUserId; + const allowedToPin = userRoles.some((role) => pinRoles.has(role)); + const allowedToReport = !isOwner; + const allowedToEdit = + userRoles.some((role) => editMessageRoles.has(role)) || isOwner; + const allowedToDelete = userRoles.some((role) => + deleteMessageRoles.has(role), + ); + const allowedToDeleteOwn = userRoles.some((role) => + deleteOwnMessageRoles.has(role), + ); + const allowedToForceDelete = userRoles.some((role) => + forceDeleteMessageRoles.has(role), + ); - const isAllowedToReport = message.u._id !== authenticatedUserId; + const visibleForMessageType = + message.files?.[0]?.type !== "audio/mpeg" && + message.files?.[0]?.type !== "video/mp4"; - const isAllowedToEditMessage = userRoles.some((role) => - editMessageRoles.has(role) - ) - ? true - : message.u._id === authenticatedUserId; + const canDelete = allowedToForceDelete + ? true + : allowedToDelete + ? true + : allowedToDeleteOwn + ? isOwner + : false; - const isAllowedToDeleteMessage = userRoles.some((role) => - deleteMessageRoles.has(role) - ); - const isAllowedToDeleteOwnMessage = userRoles.some((role) => - deleteOwnMessageRoles.has(role) - ); - const isAllowedToForceDeleteMessage = userRoles.some((role) => - forceDeleteMessageRoles.has(role) - ); - - const isVisibleForMessageType = - message.files?.[0].type !== 'audio/mpeg' && - message.files?.[0].type !== 'video/mp4'; - - const canDeleteMessage = isAllowedToForceDeleteMessage - ? true - : isAllowedToDeleteMessage - ? true - : isAllowedToDeleteOwnMessage - ? message.u._id === authenticatedUserId - : false; + return { + isAllowedToPin: allowedToPin, + isAllowedToReport: allowedToReport, + isAllowedToEditMessage: allowedToEdit, + isAllowedToDeleteMessage: allowedToDelete, + isAllowedToDeleteOwnMessage: allowedToDeleteOwn, + isAllowedToForceDeleteMessage: allowedToForceDelete, + isVisibleForMessageType: visibleForMessageType, + canDeleteMessage: canDelete, + }; + }, [ + authenticatedUserId, + userRoles, + pinRoles, + deleteMessageRoles, + deleteOwnMessageRoles, + forceDeleteMessageRoles, + editMessageRoles, + message.u._id, + message.files, + ]); const options = useMemo( () => ({ reply: { - label: 'Reply in thread', - id: 'reply', + label: "Reply in thread", + id: "reply", onClick: handleOpenThread(message), - iconName: 'thread', + iconName: "thread", visible: !isThreadMessage, }, quote: { - label: 'Quote', - id: 'quote', + label: "Quote", + id: "quote", onClick: () => handleQuoteMessage(message), - iconName: 'quote', + iconName: "quote", visible: true, }, star: { label: message.starred && message.starred.find((u) => u._id === authenticatedUserId) - ? 'Unstar' - : 'Star', - id: 'star', + ? "Unstar" + : "Star", + id: "star", onClick: () => handleStarMessage(message), iconName: message.starred && message.starred.find((u) => u._id === authenticatedUserId) - ? 'star-filled' - : 'star', + ? "star-filled" + : "star", visible: true, }, reaction: { - label: 'Add reaction', - id: 'reaction', + label: "Add reaction", + id: "reaction", onClick: () => setEmojiOpen(true), - iconName: 'emoji', + iconName: "emoji", visible: true, }, pin: { - label: message.pinned ? 'Unpin' : 'Pin', - id: 'pin', + label: message.pinned ? "Unpin" : "Pin", + id: "pin", onClick: () => handlePinMessage(message), - iconName: message.pinned ? 'pin-filled' : 'pin', + iconName: message.pinned ? "pin-filled" : "pin", visible: isAllowedToPin, }, edit: { - label: 'Edit', - id: 'edit', + label: "Edit", + id: "edit", onClick: () => handleEditMessage(message), - iconName: 'edit', + iconName: "edit", visible: isAllowedToEditMessage, - color: isEditing ? 'secondary' : 'default', + color: isEditing ? "secondary" : "default", ghost: !isEditing, }, copy: { - label: 'Copy message', - id: 'copy', + label: "Copy message", + id: "copy", onClick: () => handleCopyMessage(message), - iconName: 'copy', + iconName: "copy", visible: true, }, link: { - label: 'Copy link', - id: 'link', + label: "Copy link", + id: "link", onClick: () => handleCopyMessageLink(message), - iconName: 'link', + iconName: "link", visible: true, }, delete: { - label: 'Delete', - id: 'delete', + label: "Delete", + id: "delete", onClick: () => setShowDeleteModal(true), - iconName: 'trash', + iconName: "trash", visible: canDeleteMessage, - type: 'destructive', + type: "destructive", }, report: { - label: 'Report', - id: 'report', + label: "Report", + id: "report", onClick: () => handlerReportMessage(message), - iconName: 'report', + iconName: "report", visible: isAllowedToReport, - type: 'destructive', + type: "destructive", }, }), [ @@ -210,8 +237,12 @@ export const MessageToolbox = ({ handleEditMessage, handlerReportMessage, handleCopyMessage, + handleCopyMessageLink, isAllowedToPin, - ] + isAllowedToReport, + isAllowedToEditMessage, + canDeleteMessage, + ], ); const menuOptions = menuItems @@ -248,7 +279,7 @@ export const MessageToolbox = ({ @@ -259,9 +290,9 @@ export const MessageToolbox = ({ )} @@ -288,36 +319,36 @@ export const MessageToolbox = ({ {' '} + style={{ marginRight: "0.5rem" }} + />{" "} Delete this message? {message.file ? ( - message.file.type.startsWith('image/') ? ( + message.file.type.startsWith("image/") ? ( {`${message.file.name} (${( message.file.size / 1024 ).toFixed(2)} kB)`} - ) : message.file.type.startsWith('video/') ? ( + ) : message.file.type.startsWith("video/") ? ( Your browser does not support the video tag. - ) : message.file.type.startsWith('audio/') ? ( - + ) : message.file.type.startsWith("audio/") ? ( + 0 && message.msg && - message.msg[0] === '[' && + message.msg[0] === "[" && message.attachments.map((attachment, index) => ( { - const { theme } = useTheme(); - const { mode } = useTheme(); + const { theme, mode } = useTheme(); const styles = getMessageAggregatorStyles(theme); const setExclusiveState = useSetExclusiveState(); const { ECOptions } = useRCContext(); @@ -128,12 +128,17 @@ export const MessageAggregator = ({ } }; - const isMessageNewDay = (current, previous) => - !previous || - shouldRender(previous) || - !isSameDay(new Date(current.ts), new Date(previous.ts)); + useEffect(() => { + const hasRendered = messageList.some((msg) => shouldRender(msg)); + setMessageRendered(hasRendered); + }, [messageList, shouldRender]); - const noMessages = messageList?.length === 0 || !messageRendered; + const isMessageNewDay = (current, previous) => { + if (!previous || shouldRender(previous)) return true; + return !isSameDay(new Date(current.ts), new Date(previous.ts)); + }; + + const noMessages = (messageList?.length === 0 || !messageRendered) && !fetching && !loading; const ViewComponent = viewType === 'Popup' ? Popup : Sidebar; return ( @@ -167,69 +172,80 @@ export const MessageAggregator = ({ )} - {[...new Map(messageList.map((msg) => [msg._id, msg])).values()].map( - (msg, index, arr) => { - const newDay = isMessageNewDay(msg, arr[index - 1]); - if (!messageRendered && shouldRender(msg)) { - setMessageRendered(true); - } - - return ( - - {type === 'message' && newDay && ( - - {format(new Date(msg.ts), 'MMMM d, yyyy')} - - )} - {type === 'file' ? ( - { + const newDay = isMessageNewDay(msg, arr[index - 1]); + + return ( + + {type === 'message' && newDay && ( + + {format(new Date(msg.ts), 'MMMM d, yyyy')} + + )} + {type === 'file' ? ( + + ) : ( + + - ) : ( - + + setJumpToMessage(msg)} + css={{ + position: 'relative', + zIndex: 10, + marginRight: '5px', }} > - - - setJumpToMessage(msg)} - css={{ - position: 'relative', - zIndex: 10, - marginRight: '5px', - }} - > - - - - )} - - ); - } - )} + + + + )} + + ); + })} )} ); }; + +MessageAggregator.propTypes = { + title: PropTypes.string.isRequired, + iconName: PropTypes.string, + noMessageInfo: PropTypes.string, + shouldRender: PropTypes.func.isRequired, + fetchedMessageList: PropTypes.array, + filterProps: PropTypes.object, + searchProps: PropTypes.object, + searchFiltered: PropTypes.array, + fetching: PropTypes.bool, + type: PropTypes.oneOf(['message', 'file']), + viewType: PropTypes.oneOf(['Sidebar', 'Popup']), +}; + +export default MessageAggregator; diff --git a/packages/react/src/views/TypingUsers/TypingUsers.js b/packages/react/src/views/TypingUsers/TypingUsers.js index db05619ec1..9982e6f6ab 100644 --- a/packages/react/src/views/TypingUsers/TypingUsers.js +++ b/packages/react/src/views/TypingUsers/TypingUsers.js @@ -11,10 +11,11 @@ export default function TypingUsers() { const { theme } = useTheme(); useEffect(() => { - RCInstance.addTypingStatusListener((t) => { + const handleTypingStatus = (t) => { setTypingUsers((t || []).filter((u) => u !== currentUserName)); - }); - return () => RCInstance.removeTypingStatusListener(setTypingUsers); + }; + RCInstance.addTypingStatusListener(handleTypingStatus); + return () => RCInstance.removeTypingStatusListener(handleTypingStatus); }, [RCInstance, setTypingUsers, currentUserName]); const typingStatusMessage = useMemo(() => {