import { Box, Icon, Icons, PopOut, RectCords, Text, Tooltip, TooltipProvider, as, color, config, } from 'folds'; import React, { useEffect, useState } from 'react'; import { useLongPress } from '../../../hooks/useLongPress'; import { DiffToken, countChanges } from '../../../utils/wordDiff'; import { useEditDiff } from './EditDiffContext'; const warningStyle = { color: color.Warning.Main, opacity: config.opacity.P300 }; const criticalStyle = { color: color.Critical.Main, opacity: config.opacity.P300 }; export const MessageDeletedContent = as<'div', { children?: never; reason?: string }>( ({ reason, ...props }, ref) => ( {reason ? `This message has been deleted — ${reason}` : 'This message has been deleted'} ), ); export const MessageUnsupportedContent = as<'div', { children?: never }>(({ ...props }, ref) => ( Unsupported message )); export const MessageFailedContent = as<'div', { children?: never }>(({ ...props }, ref) => ( Failed to load message )); export const MessageBadEncryptedContent = as<'div', { children?: never }>(({ ...props }, ref) => ( Unable to decrypt message )); export const MessageNotDecryptedContent = as<'div', { children?: never }>(({ ...props }, ref) => ( This message is not decrypted yet )); export const MessageBrokenContent = as<'div', { children?: never }>(({ ...props }, ref) => ( Broken message )); export const MessageEmptyContent = as<'div', { children?: never }>(({ ...props }, ref) => ( Empty message )); export const MessageVerificationRequestContent = as<'div', { children?: never }>( ({ ...props }, ref) => ( Device verification request — open another Matrix client to accept ), ); const editDiffStyle: React.CSSProperties = { maxWidth: '40ch', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', maxHeight: '12lh', overflow: 'hidden', }; /** The last edit as a word diff: removed words struck, added words bold. */ export function EditDiffBody({ tokens, hint }: { tokens: DiffToken[]; hint: string }) { const { added, removed } = countChanges(tokens); return ( Last edit {added > 0 && ` · +${added}`} {removed > 0 && ` · −${removed}`} {tokens.map((t, i) => { if (t.op === 'add') return ( {t.text} ); if (t.op === 'del') return ( {t.text} ); return {t.text}; })} {hint} ); } const editedButtonStyle: React.CSSProperties = { cursor: 'pointer', background: 'none', border: 'none', padding: 0, }; /** * "(edited)" label. With an edit-history handler it is a button that opens * the viewer; when the surrounding message also provides a last-edit diff * (see `EditDiffContext`) hover/focus shows it as a tooltip and a touch * long-press shows it as a popout. [Gitea #144] */ export const MessageEditedContent = as< 'span', { children?: never; onEditHistoryClick?: () => void } >(({ onEditHistoryClick, ...props }, ref) => { const diff = useEditDiff(); const [pressAnchor, setPressAnchor] = useState(); const { coarse, handlers, suppressContextMenu } = useLongPress((x, y) => { if (diff) setPressAnchor({ x, y, width: 1, height: 1 }); }); useEffect(() => { if (!pressAnchor) return undefined; const close = () => setPressAnchor(undefined); const t = window.setTimeout(close, 6000); window.addEventListener('touchstart', close, { passive: true }); window.addEventListener('scroll', close, { passive: true, capture: true }); return () => { window.clearTimeout(t); window.removeEventListener('touchstart', close); window.removeEventListener('scroll', close, { capture: true }); }; }, [pressAnchor]); if (!onEditHistoryClick) { return ( {' (edited)'} ); } const button = (tipRef?: React.RefCallback) => ( ); return ( )}> {diff ? ( } > {(tipRef) => button(tipRef)} ) : ( button() )} {diff && pressAnchor && ( } /> )} ); }); type TranslatedStatus = 'downloading' | 'translating' | 'done' | 'error'; const translatedLabel = ( status: TranslatedStatus, fromLangName: string, downloadProgress?: number, ): string => { if (status === 'downloading') { const pct = typeof downloadProgress === 'number' ? ` ${Math.round(downloadProgress * 100)}%` : ''; return `Downloading translation model…${pct}`; } if (status === 'translating') return 'Translating…'; if (status === 'error') return 'Translation failed — show original'; return `Translated from ${fromLangName} · Show original`; }; // Inline chip shown beside a translated message body. Clicking it toggles back // to the original text (the same per-event toggle the message menu drives). export const MessageTranslatedContent = as< 'span', { children?: never; fromLangName: string; status: TranslatedStatus; downloadProgress?: number; onToggle: () => void; } >(({ fromLangName, status, downloadProgress, onToggle, ...props }, ref) => ( )}> ));