mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-07-30 21:43:14 +02:00
Fix giphy rendering issue. Closes #291.
This commit is contained in:
@@ -16,7 +16,12 @@ import {
|
||||
formatTime,
|
||||
parseSenderFromText,
|
||||
} from '../utils/messageParser';
|
||||
import { giphyUrlForId, parseGif, parseReaction } from '../utils/meshcoreOpenPayloads';
|
||||
import {
|
||||
giphyUrlForId,
|
||||
parseGif,
|
||||
parseReaction,
|
||||
splitReplyMention,
|
||||
} from '../utils/meshcoreOpenPayloads';
|
||||
import { useRichPayloads } from '../contexts/RichPayloadContext';
|
||||
import { formatHopCounts, type SenderInfo } from '../utils/pathUtils';
|
||||
import { getDirectContactRoute } from '../utils/pathUtils';
|
||||
@@ -89,20 +94,49 @@ function ReactionPayload({ emoji }: { emoji: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Recognize a whole-message MeshCore Open payload and render it. Returns null
|
||||
// when the content is not a recognized payload, so the caller renders normally.
|
||||
function renderMeshcoreOpenPayload(content: string): ReactNode | null {
|
||||
const gifId = parseGif(content);
|
||||
// Render a bare payload body (no reply prefix) into its rich node, or null.
|
||||
function renderPayloadBody(body: string): ReactNode | null {
|
||||
const gifId = parseGif(body);
|
||||
if (gifId) {
|
||||
return <GifPayload gifId={gifId} rawText={content} />;
|
||||
return <GifPayload gifId={gifId} rawText={body} />;
|
||||
}
|
||||
const reaction = parseReaction(content);
|
||||
const reaction = parseReaction(body);
|
||||
if (reaction) {
|
||||
return <ReactionPayload emoji={reaction.emoji} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recognize a MeshCore Open payload and render it. Handles both a whole-message
|
||||
// payload ("g:<id>") and a reply-prefixed one ("@[Name] g:<id>") — the form
|
||||
// meshcore-open sends when a GIF/reaction is a reply, which otherwise renders as
|
||||
// raw text (issue #291). Returns null when the content is not a recognized
|
||||
// payload, so the caller renders normally.
|
||||
function renderMeshcoreOpenPayload(
|
||||
content: string,
|
||||
radioName?: string,
|
||||
onChannelReferenceClick?: (channelName: string) => void
|
||||
): ReactNode | null {
|
||||
const whole = renderPayloadBody(content);
|
||||
if (whole) return whole;
|
||||
|
||||
const split = splitReplyMention(content);
|
||||
if (split) {
|
||||
const body = renderPayloadBody(split.body);
|
||||
if (body) {
|
||||
// Preserve the reply mention (rendered as a normal @[Name] mention) so the
|
||||
// GIF/reaction still reads as a reply to that person.
|
||||
return (
|
||||
<span className="inline-flex flex-wrap items-center gap-1.5">
|
||||
{renderTextWithMentions(split.mention, radioName, onChannelReferenceClick)}
|
||||
{body}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// URL regex for linkifying plain text
|
||||
const URL_PATTERN =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
|
||||
@@ -1079,7 +1113,8 @@ export function MessageList({
|
||||
</div>
|
||||
)}
|
||||
<div className="break-words whitespace-pre-wrap">
|
||||
{(renderRichPayloads && renderMeshcoreOpenPayload(content)) ||
|
||||
{(renderRichPayloads &&
|
||||
renderMeshcoreOpenPayload(content, radioName, onChannelReferenceClick)) ||
|
||||
content.split('\n').map((line, i, arr) => (
|
||||
<span key={i}>
|
||||
{renderTextWithMentions(line, radioName, onChannelReferenceClick)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
giphyUrlForId,
|
||||
parseGif,
|
||||
parseReaction,
|
||||
splitReplyMention,
|
||||
} from '../utils/meshcoreOpenPayloads';
|
||||
|
||||
describe('parseGif', () => {
|
||||
@@ -79,3 +80,41 @@ describe('parseReaction', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitReplyMention', () => {
|
||||
it('splits a reply-prefixed gif into mention + body (issue #291)', () => {
|
||||
// meshcore-open sends GIF replies as "@[senderName] g:<id>".
|
||||
expect(splitReplyMention('@[Alice] g:abc123')).toEqual({
|
||||
mention: '@[Alice]',
|
||||
body: 'g:abc123',
|
||||
});
|
||||
});
|
||||
|
||||
it('the split body parses as a gif while the whole string does not', () => {
|
||||
const whole = '@[Alice] g:abc123';
|
||||
expect(parseGif(whole)).toBeNull(); // anchored regex rejects the prefix
|
||||
const split = splitReplyMention(whole);
|
||||
expect(split && parseGif(split.body)).toBe('abc123');
|
||||
});
|
||||
|
||||
it('splits a reply-prefixed reaction', () => {
|
||||
expect(splitReplyMention('@[Bob] r:1a2b:00')).toEqual({
|
||||
mention: '@[Bob]',
|
||||
body: 'r:1a2b:00',
|
||||
});
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace and preserves names with spaces', () => {
|
||||
expect(splitReplyMention(' @[Node One] g:xy ')).toEqual({
|
||||
mention: '@[Node One]',
|
||||
body: 'g:xy',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null without a leading reply mention', () => {
|
||||
expect(splitReplyMention('g:abc123')).toBeNull();
|
||||
expect(splitReplyMention('hello world')).toBeNull();
|
||||
expect(splitReplyMention('@[Alice]')).toBeNull(); // mention only, no body
|
||||
expect(splitReplyMention('text @[Alice] g:abc')).toBeNull(); // not a leading mention
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,3 +111,29 @@ export function parseReaction(text: string): ParsedReaction | null {
|
||||
}
|
||||
return { emoji: REACTION_EMOJIS[index], targetHash: match[1] };
|
||||
}
|
||||
|
||||
// --- Reply-mention prefix (@[senderName] <payload>) ---
|
||||
|
||||
// meshcore-open prefixes replies with "@[senderName] " before the message body
|
||||
// (see meshcore-open channels.md / BLE_PROTOCOL.md). Its own display code strips
|
||||
// that prefix before parsing rich payloads, so a GIF/reaction reply arrives on
|
||||
// the wire as "@[Name] g:<id>". parseGif/parseReaction stay strict (whole-body
|
||||
// only); this splits the reply prefix off so the remainder can be parsed.
|
||||
const REPLY_MENTION_PREFIX = /^(@\[[^\]]+\])\s+([\s\S]+)$/;
|
||||
|
||||
export interface SplitReplyMention {
|
||||
/** The leading "@[Name]" reply-mention token. */
|
||||
mention: string;
|
||||
/** The message remainder after the reply-mention prefix. */
|
||||
body: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a leading meshcore-open reply mention ("@[Name] ") off the text, or
|
||||
* return null when there is no such prefix.
|
||||
*/
|
||||
export function splitReplyMention(text: string): SplitReplyMention | null {
|
||||
const match = REPLY_MENTION_PREFIX.exec(text.trim());
|
||||
if (!match) return null;
|
||||
return { mention: match[1], body: match[2] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user