/* * Wire * Copyright (C) 2021 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/. * */ import {useEffect, FC, useState, HTMLProps, useRef} from 'react'; import {isKeyDownEvent} from 'src/script/guards/Event'; import {isAuxClickEvent, isClickEvent} from 'src/script/guards/Mouse'; import {getAllFocusableElements, setElementsTabIndex} from 'Util/focusUtil'; import {handleKeyDown} from 'Util/KeyboardUtil'; import {ShowMoreButton} from './ShowMoreButton'; export type ElementType = 'markdownLink' | 'email' | 'mention'; interface TextMessageRendererProps extends HTMLProps { onMessageClick: (event: MouseEvent | KeyboardEvent, elementType: ElementType, messageDetails: MessageDetails) => void; text: string; isFocusable: boolean; /** will collapse the text to a single line when set (and add a `showMore` button if there is more content to show) */ collapse?: boolean; setCanShowMore?: (showMore: boolean) => void; } export interface MessageDetails { href?: string; userId?: string; userDomain?: string; } const TextMessage: FC = ({ text, onMessageClick, isFocusable, className, collapse = false, ...props }) => { const containerRef = useRef(null); const [canShowMore, setCanShowMore] = useState(false); const [showFullText, setShowFullText] = useState(!collapse); const collapsedHeightRef = useRef(0); useEffect(() => { const element = containerRef.current; if (element && collapse) { const preNode = element.querySelector('pre'); const collapsedHeight = collapsedHeightRef.current || element.clientHeight; const width = Math.max(element.scrollWidth, preNode ? preNode.scrollWidth : 0); const height = Math.max(element.scrollHeight, preNode ? preNode.scrollHeight : 0); const isWider = width > element.clientWidth; const isHigher = height > collapsedHeight; collapsedHeightRef.current = collapsedHeight; setCanShowMore?.(isWider || isHigher); } }, [collapse, setCanShowMore]); useEffect(() => { const element = containerRef.current; if (!element) { return; } const interactiveMsgElements = getAllFocusableElements(element); setElementsTabIndex(interactiveMsgElements, isFocusable); }, [isFocusable]); const forwardEvent = ( event: KeyboardEvent | MouseEvent, elementType: ElementType, messageDetails: MessageDetails, ) => { if (isKeyDownEvent(event) && isFocusable) { handleKeyDown(event, () => { event.preventDefault(); onMessageClick(event, elementType, messageDetails); }); } else if (isClickEvent(event) || isAuxClickEvent(event)) { event.preventDefault(); onMessageClick(event, elementType, messageDetails); } }; /** * Will handle interaction with the message. * Depending on the child element clicked, it will forward the event to the parent component. */ const handleInteraction = (event: React.MouseEvent | React.KeyboardEvent) => { const target = event.target as HTMLElement; if (!target) { return; } const emailElement = target.closest('[data-email-link]'); const markdownLinkElement = target.closest('[data-md-link]'); const mentionElement = target.closest('.message-mention'); if (markdownLinkElement) { const href = (markdownLinkElement as HTMLAnchorElement).href; const markdownLinkDetails = { href: href, }; forwardEvent(event.nativeEvent, 'markdownLink', markdownLinkDetails); } else if (emailElement) { const href = (emailElement as HTMLAnchorElement).href; const markdownLinkDetails = { href: href, }; forwardEvent(event.nativeEvent, 'email', markdownLinkDetails); } else if (mentionElement) { const mentionMsgDetails = { userId: target.dataset.userId, userDomain: target.dataset.userDomain, }; forwardEvent(event.nativeEvent, 'mention', mentionMsgDetails); } }; const extraClasses = showFullText ? 'message-quote__text--full' : ''; const toggleShowMore = () => setShowFullText(prev => !prev); return ( <> { // We will register the click event on the paragraph element and determine the type of the element clicked. // //This is because the paragraph element is fed with raw HTML and we cannot register the click event on the //clickabled elements directly. } {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}

{canShowMore && ( )} ); }; export const TextMessageRenderer = (props: TextMessageRendererProps) => { // We want to make sure that this element is re-rendered when the text changes (this will trigger useEffects' calculations to run). return ; };