Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-room-abbreviations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Add room abbreviations: moderators can define a list of term/definition pairs in room settings; defined terms are highlighted with a hover tooltip in plain-text messages.
79 changes: 79 additions & 0 deletions src/app/components/message/RenderBody.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,60 @@
import { MouseEventHandler, useEffect, useState } from 'react';
import parse, { HTMLReactParserOptions } from 'html-react-parser';
import Linkify from 'linkify-react';
import { Opts } from 'linkifyjs';
import { PopOut, RectCords, Text, Tooltip, TooltipProvider, toRem } from 'folds';
import { sanitizeCustomHtml } from '$utils/sanitize';
import { highlightText, scaleSystemEmoji } from '$plugins/react-custom-html-parser';
import { useRoomAbbreviationsContext } from '$hooks/useRoomAbbreviations';
import { splitByAbbreviations } from '$utils/abbreviations';
import { MessageEmptyContent } from './content';

type AbbreviationTermProps = {
text: string;
definition: string;
};
function AbbreviationTerm({ text, definition }: AbbreviationTermProps) {
const [anchor, setAnchor] = useState<RectCords | undefined>();

const handleClick: MouseEventHandler<HTMLElement> = (e) => {
e.stopPropagation();
setAnchor((prev) => (prev ? undefined : e.currentTarget.getBoundingClientRect()));
};

// On mobile, tapping an abbreviation pins the tooltip open.
// Tapping anywhere else (outside the abbr) dismisses it.
useEffect(() => {
if (!anchor) return undefined;
const dismiss = () => setAnchor(undefined);
document.addEventListener('click', dismiss, { once: true });
return () => document.removeEventListener('click', dismiss);
}, [anchor]);

const tooltipContent = (
<Tooltip style={{ maxWidth: toRem(250) }}>
<Text size="T200">{definition}</Text>
</Tooltip>
);

return (
<>
<TooltipProvider position="Top" tooltip={tooltipContent}>
{(triggerRef) => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
<abbr ref={triggerRef as React.Ref<HTMLElement>} onClick={handleClick}>
{text}
</abbr>
)}
</TooltipProvider>
{anchor && (
<PopOut anchor={anchor} position="Top" align="Center" content={tooltipContent}>
{null}
</PopOut>
)}
</>
);
}

type RenderBodyProps = {
body: string;
customBody?: string;
Expand All @@ -20,12 +70,41 @@ export function RenderBody({
htmlReactParserOptions,
linkifyOpts,
}: Readonly<RenderBodyProps>) {
const abbrMap = useRoomAbbreviationsContext();

if (customBody) {
if (customBody === '') return <MessageEmptyContent />;
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
}
if (body === '') return <MessageEmptyContent />;

if (abbrMap.size > 0) {
const segments = splitByAbbreviations(body, abbrMap);
if (segments.some((s) => s.termKey !== undefined)) {
return (
<>
{segments.map((seg, i) => {
if (seg.termKey !== undefined) {
const definition = abbrMap.get(seg.termKey) ?? '';
return (
// eslint-disable-next-line react/no-array-index-key
<AbbreviationTerm key={i} text={seg.text} definition={definition} />
);
}
return (
// eslint-disable-next-line react/no-array-index-key
<Linkify key={i} options={linkifyOpts}>
{highlightRegex
? highlightText(highlightRegex, scaleSystemEmoji(seg.text))
: scaleSystemEmoji(seg.text)}
</Linkify>
);
})}
</>
);
}
}

return (
<Linkify options={linkifyOpts}>
{highlightRegex
Expand Down
9 changes: 9 additions & 0 deletions src/app/features/room-settings/RoomSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DeveloperTools } from '$features/common-settings/developer-tools';
import { Cosmetics } from '$features/common-settings/cosmetics/Cosmetics';
import { Permissions } from './permissions';
import { General } from './general';
import { RoomAbbreviations } from './abbreviations/RoomAbbreviations';

type RoomSettingsMenuItem = {
page: RoomSettingsPage;
Expand Down Expand Up @@ -51,6 +52,11 @@ const useRoomSettingsMenuItems = (): RoomSettingsMenuItem[] =>
icon: Icons.Alphabet,
activeIcon: Icons.AlphabetUnderline,
},
{
page: RoomSettingsPage.AbbreviationsPage,
name: 'Abbreviations',
icon: Icons.Info,
},
{
page: RoomSettingsPage.EmojisStickersPage,
name: 'Emojis & Stickers',
Expand Down Expand Up @@ -196,6 +202,9 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
{activePage === RoomSettingsPage.DeveloperToolsPage && (
<DeveloperTools requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.AbbreviationsPage && (
<RoomAbbreviations requestClose={handlePageRequestClose} />
)}
</PageRoot>
</SwipeableOverlayWrapper>
);
Expand Down
Loading
Loading