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) => (
)}>
));