From a3f97079ab99b5e1dc0bec807e62f2dcd276fdd2 Mon Sep 17 00:00:00 2001 From: Przemyslaw Jozwik Date: Fri, 31 May 2024 12:59:28 +0200 Subject: [PATCH 1/3] feat: Reaction Popover is stuck on a message or several messages --- .../Message/ContentMessage/ContentMessage.tsx | 27 +++++++- .../Message/{index.tsx => Message.tsx} | 61 +++++++++---------- .../MessagesList/Message/MessageWrapper.tsx | 3 +- .../components/MessagesList/Message/index.ts | 20 ++++++ .../content/conversation/message-list.less | 27 ++++---- 5 files changed, 88 insertions(+), 50 deletions(-) rename src/script/components/MessagesList/Message/{index.tsx => Message.tsx} (84%) create mode 100644 src/script/components/MessagesList/Message/index.ts diff --git a/src/script/components/MessagesList/Message/ContentMessage/ContentMessage.tsx b/src/script/components/MessagesList/Message/ContentMessage/ContentMessage.tsx index 9b64a73a617..6b8ceb894d0 100644 --- a/src/script/components/MessagesList/Message/ContentMessage/ContentMessage.tsx +++ b/src/script/components/MessagesList/Message/ContentMessage/ContentMessage.tsx @@ -17,7 +17,7 @@ * */ -import {useMemo, useState, useEffect} from 'react'; +import {useMemo, useState, useEffect, useCallback, useRef} from 'react'; import {QualifiedId} from '@wireapp/api-client/lib/user'; import cx from 'classnames'; @@ -47,6 +47,7 @@ import {ContextMenuEntry} from '../../../../ui/ContextMenu'; import {EphemeralTimer} from '../EphemeralTimer'; import {MessageTime} from '../MessageTime'; import {useMessageFocusedTabIndex} from '../util'; + export interface ContentMessageProps extends Omit { contextMenu: {entries: ko.Subscribable}; conversation: Conversation; @@ -86,6 +87,7 @@ export const ContentMessageComponent = ({ onClickReaction, onClickDetails, }: ContentMessageProps) => { + const messageRef = useRef(null); // check if current message is focused and its elements focusable const msgFocusState = useMemo(() => isMsgElementsFocusable && isFocused, [isMsgElementsFocusable, isFocused]); const messageFocusedTabIndex = useMessageFocusedTabIndex(msgFocusState); @@ -125,6 +127,7 @@ export const ContentMessageComponent = ({ const [isActionMenuVisible, setActionMenuVisibility] = useState(false); const isMenuOpen = useMessageActionsState(state => state.isMenuOpen); + useEffect(() => { setActionMenuVisibility(isFocused || msgFocusState); }, [msgFocusState, isFocused]); @@ -132,6 +135,8 @@ export const ContentMessageComponent = ({ const isConversationReadonly = conversation.readOnlyState() !== null; const contentMessageWrapperRef = (element: HTMLDivElement | null) => { + messageRef.current = element; + setTimeout(() => { if (element?.parentElement?.querySelector(':hover') === element) { // Trigger the action menu in case the component is rendered with the mouse already hovering over it @@ -148,6 +153,26 @@ export const ContentMessageComponent = ({ const isAssetMessage = isFileMessage || isAudioMessage || isVideoMessage || isImageMessage; + const handleOutsideClick = useCallback((event: Event) => { + if (!messageRef.current) { + return; + } + + event.preventDefault(); + + if (!messageRef.current.contains(event.target as Node)) { + setActionMenuVisibility(false); + } + }, []); + + useEffect(() => { + window.addEventListener('click', handleOutsideClick); + + return () => { + window.removeEventListener('click', handleOutsideClick); + }; + }, [handleOutsideClick]); + return (
void; @@ -76,13 +76,13 @@ export interface MessageParams extends MessageActions { isFocused: boolean; /** will visually highlight the message when it's being loaded */ isHighlighted: boolean; - handleFocus: (id: string) => void; + handleFocus: (id?: string) => void; handleArrowKeyDown: (e: React.KeyboardEvent) => void; isMsgElementsFocusable: boolean; setMsgElementsFocusable: (isMsgElementsFocusable: boolean) => void; } -export const Message: React.FC = props => { +export const Message = (props: MessageParams & {scrollTo?: ScrollToElement}) => { const { message, isHighlighted, @@ -95,8 +95,10 @@ export const Message: React.FC = p isMsgElementsFocusable, setMsgElementsFocusable, } = props; + const messageElementRef = useRef(null); - const messageRef = useRef(null); + // const messageRef = useRef(null); + const {status, ephemeral_expires} = useKoSubscribableChildren(message, ['status', 'ephemeral_expires']); const messageFocusedTabIndex = useMessageFocusedTabIndex(isFocused); @@ -104,6 +106,7 @@ export const Message: React.FC = p if (!messageElementRef.current) { return; } + if (isHighlighted) { scrollTo?.({center: true, element: messageElementRef.current}); // for reply message, focus on the original message when original message link is clicked for keyboard users @@ -114,7 +117,7 @@ export const Message: React.FC = p const handleDivKeyDown = (event: React.KeyboardEvent) => { // when a message is focused set its elements focusable if (!event.shiftKey && isTabKey(event)) { - if (!messageRef.current) { + if (!messageElementRef.current) { return; } setMsgElementsFocusable(true); @@ -130,21 +133,21 @@ export const Message: React.FC = p useEffect(() => { // Move element into view when it is focused if (isFocused) { - messageRef.current?.focus(); + messageElementRef.current?.focus(); } }, [isFocused]); - // set message elements focus for non content type mesages - // some non content type message has interactive element like invite people for member message + // set message elements focus for non-content type messages + // some non-content type message has interactive element like invite people for member message useEffect(() => { - if (!messageRef.current || message.isContent()) { + if (!messageElementRef.current || message.isContent()) { return; } - const interactiveMsgElements = getAllFocusableElements(messageRef.current); + const interactiveMsgElements = getAllFocusableElements(messageElementRef.current); setElementsTabIndex(interactiveMsgElements, isMsgElementsFocusable && isFocused); }, [isFocused, isMsgElementsFocusable, message]); - const content = ( + const messageContent = ( = p /> ); - const wrappedContent = onVisible ? ( - - {content} - - ) : ( - content - ); - return ( + /*eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/
handleFocus(message.id)} > - {/*eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions*/} -
handleFocus(message.id)} - className="message-wrapper" - > - {wrappedContent} -
+ {onVisible ? ( + + {messageContent} + + ) : ( + messageContent + )}
); }; diff --git a/src/script/components/MessagesList/Message/MessageWrapper.tsx b/src/script/components/MessagesList/Message/MessageWrapper.tsx index f9f19791d94..97c50630fc1 100644 --- a/src/script/components/MessagesList/Message/MessageWrapper.tsx +++ b/src/script/components/MessagesList/Message/MessageWrapper.tsx @@ -44,6 +44,7 @@ import {FederationStopMessage} from './FederationStopMessage'; import {FileTypeRestrictedMessage} from './FileTypeRestrictedMessage'; import {LegalHoldMessage} from './LegalHoldMessage'; import {MemberMessage} from './MemberMessage'; +import {MessageParams} from './Message'; import {MissedMessage} from './MissedMessage'; import {PingMessage} from './PingMessage'; import {SystemMessage} from './SystemMessage'; @@ -55,8 +56,6 @@ import {CompositeMessage} from '../../../entity/message/CompositeMessage'; import {TeamState} from '../../../team/TeamState'; import {ContextMenuEntry} from '../../../ui/ContextMenu'; -import {MessageParams} from './index'; - const isOutgoingQuote = (quoteEntity: QuoteEntity): quoteEntity is OutgoingQuote => { return quoteEntity.hash !== undefined; }; diff --git a/src/script/components/MessagesList/Message/index.ts b/src/script/components/MessagesList/Message/index.ts new file mode 100644 index 00000000000..9f91748fbdf --- /dev/null +++ b/src/script/components/MessagesList/Message/index.ts @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export * from './Message'; diff --git a/src/style/content/conversation/message-list.less b/src/style/content/conversation/message-list.less index bebf0e61026..dd55975912d 100644 --- a/src/style/content/conversation/message-list.less +++ b/src/style/content/conversation/message-list.less @@ -37,6 +37,18 @@ // MESSAGE .message { position: relative; + outline-offset: -0.1rem; + + &.content-message { + &:hover, + &:focus-visible { + background-color: #fff; + + body.theme-dark & { + background-color: var(--gray-90); + } + } + } & * { .accent-selection; @@ -87,7 +99,7 @@ .content-message { &:has(.message-header) { - padding-top: 6px; + margin-top: 6px; } } @@ -107,15 +119,6 @@ .content-message-wrapper { padding-block: 6px; - - &:hover, - &:focus-visible { - background-color: #fff; - - body.theme-dark & { - background-color: var(--gray-90); - } - } } .message-marked { @@ -264,10 +267,6 @@ height: 16px; } -.message-wrapper { - outline-offset: -0.1rem; -} - .message-mention { outline-offset: 0.4rem; } From bacd2f1a45994efae17b263987b74b242d4447fa Mon Sep 17 00:00:00 2001 From: Przemyslaw Jozwik Date: Fri, 31 May 2024 13:04:08 +0200 Subject: [PATCH 2/3] remove unecessary code --- src/script/components/MessagesList/Message/Message.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/script/components/MessagesList/Message/Message.tsx b/src/script/components/MessagesList/Message/Message.tsx index 53a4cf5441d..08aa9d7dfa7 100644 --- a/src/script/components/MessagesList/Message/Message.tsx +++ b/src/script/components/MessagesList/Message/Message.tsx @@ -97,7 +97,6 @@ export const Message = (props: MessageParams & {scrollTo?: ScrollToElement}) => } = props; const messageElementRef = useRef(null); - // const messageRef = useRef(null); const {status, ephemeral_expires} = useKoSubscribableChildren(message, ['status', 'ephemeral_expires']); const messageFocusedTabIndex = useMessageFocusedTabIndex(isFocused); From c153af8fe548757123af582de0fb31fa666a4a20 Mon Sep 17 00:00:00 2001 From: Przemyslaw Jozwik Date: Fri, 31 May 2024 13:30:30 +0200 Subject: [PATCH 3/3] test fixes --- src/script/components/MessagesList/Message/Message.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/script/components/MessagesList/Message/Message.tsx b/src/script/components/MessagesList/Message/Message.tsx index 08aa9d7dfa7..101105c21a4 100644 --- a/src/script/components/MessagesList/Message/Message.tsx +++ b/src/script/components/MessagesList/Message/Message.tsx @@ -17,7 +17,7 @@ * */ -import React, {useLayoutEffect, useRef, useEffect, MouseEvent} from 'react'; +import React, {useLayoutEffect, useRef, useEffect} from 'react'; import {QualifiedId} from '@wireapp/api-client/lib/user'; import cx from 'classnames';