mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 17:23:05 +02:00
Add draft reactions + gifs; region resolution
This commit is contained in:
@@ -222,6 +222,7 @@ const baseSettings = {
|
||||
advert_interval: 0,
|
||||
last_advert_time: 0,
|
||||
flood_scope: '',
|
||||
known_regions: [],
|
||||
blocked_keys: [],
|
||||
blocked_names: [],
|
||||
};
|
||||
|
||||
@@ -105,6 +105,7 @@ beforeEach(() => {
|
||||
advert_interval: 0,
|
||||
last_advert_time: 0,
|
||||
flood_scope: '',
|
||||
known_regions: [],
|
||||
blocked_keys: [],
|
||||
blocked_names: [],
|
||||
discovery_blocked_types: [],
|
||||
@@ -1147,6 +1148,7 @@ describe('SettingsFanoutSection', () => {
|
||||
advert_interval: 0,
|
||||
last_advert_time: 0,
|
||||
flood_scope: '',
|
||||
known_regions: [],
|
||||
blocked_keys: [],
|
||||
blocked_names: [],
|
||||
discovery_blocked_types: [],
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Tests for MeshCore Open rich-chat payload parsing (GIFs and reactions).
|
||||
*
|
||||
* Formats are ported from meshcore-open; see meshcoreOpenPayloads.ts.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
REACTION_EMOJIS,
|
||||
giphyUrlForId,
|
||||
parseGif,
|
||||
parseReaction,
|
||||
} from '../utils/meshcoreOpenPayloads';
|
||||
|
||||
describe('parseGif', () => {
|
||||
it('parses a g:<id> payload', () => {
|
||||
expect(parseGif('g:abc123')).toBe('abc123');
|
||||
});
|
||||
|
||||
it('accepts ids with underscores and dashes', () => {
|
||||
expect(parseGif('g:aB3_-xY')).toBe('aB3_-xY');
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace', () => {
|
||||
expect(parseGif(' g:abc123 ')).toBe('abc123');
|
||||
});
|
||||
|
||||
it('returns null for non-gif text', () => {
|
||||
expect(parseGif('hello world')).toBeNull();
|
||||
expect(parseGif('g:')).toBeNull();
|
||||
expect(parseGif('g:abc 123')).toBeNull();
|
||||
expect(parseGif('prefix g:abc')).toBeNull();
|
||||
expect(parseGif('g:abc!')).toBeNull();
|
||||
});
|
||||
|
||||
it('builds the Giphy media URL', () => {
|
||||
expect(giphyUrlForId('abc123')).toBe('https://media.giphy.com/media/abc123/giphy.gif');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseReaction', () => {
|
||||
it('decodes the first emoji (index 00)', () => {
|
||||
const result = parseReaction('r:1a2b:00');
|
||||
expect(result).toEqual({ emoji: REACTION_EMOJIS[0], targetHash: '1a2b' });
|
||||
expect(result?.emoji).toBe('👍');
|
||||
});
|
||||
|
||||
it('decodes a non-zero index', () => {
|
||||
// index 0x06 -> first smiley (after the 6 quick emojis)
|
||||
const result = parseReaction('r:ffff:06');
|
||||
expect(result?.emoji).toBe(REACTION_EMOJIS[6]);
|
||||
expect(result?.targetHash).toBe('ffff');
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace', () => {
|
||||
expect(parseReaction(' r:1a2b:00 ')?.emoji).toBe('👍');
|
||||
});
|
||||
|
||||
it('returns null for an out-of-range index', () => {
|
||||
// 0xff (255) is beyond the emoji list length
|
||||
expect(parseReaction('r:1a2b:ff')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for malformed reactions', () => {
|
||||
expect(parseReaction('r:1a2b')).toBeNull();
|
||||
expect(parseReaction('r:1a2:00')).toBeNull(); // hash too short
|
||||
expect(parseReaction('r:1A2B:00')).toBeNull(); // uppercase hex not accepted
|
||||
expect(parseReaction('r:1a2b:0')).toBeNull(); // index too short
|
||||
expect(parseReaction('hello')).toBeNull();
|
||||
});
|
||||
|
||||
it('exposes a stable, deduplication-free emoji index range', () => {
|
||||
// 6 quick + 64 smileys + 33 gestures + 32 hearts + 49 objects
|
||||
expect(REACTION_EMOJIS.length).toBe(184);
|
||||
// every defined index decodes to a string
|
||||
for (let i = 0; i < REACTION_EMOJIS.length; i++) {
|
||||
const hex = i.toString(16).padStart(2, '0');
|
||||
expect(parseReaction(`r:0000:${hex}`)?.emoji).toBe(REACTION_EMOJIS[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -62,6 +62,31 @@ describe('MessageList channel sender rendering', () => {
|
||||
expect(screen.getByTestId('corrupt-avatar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a region badge for region-scoped channel messages', () => {
|
||||
render(
|
||||
<MessageList
|
||||
messages={[createMessage({ sender_name: 'Alice', region: 'nl-gr' })]}
|
||||
contacts={[]}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('nl-gr')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Regional scope: nl-gr')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render a region badge for unscoped messages', () => {
|
||||
render(
|
||||
<MessageList
|
||||
messages={[createMessage({ sender_name: 'Alice', region: null })]}
|
||||
contacts={[]}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('nl-gr')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('prefers stored sender_name for channel messages even when text is not sender-prefixed', () => {
|
||||
render(
|
||||
<MessageList
|
||||
|
||||
@@ -91,11 +91,26 @@ describe('RawPacketDetailModal', () => {
|
||||
expect(pathRun.className).toBe(idleClassName);
|
||||
});
|
||||
|
||||
it('shows scope card with transport codes for scoped packets', () => {
|
||||
it('shows scope card with transport codes for scoped packets without a resolved region', () => {
|
||||
render(<RawPacketDetailModal packet={SCOPED_PACKET} channels={[]} onClose={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText('Scope')).toBeInTheDocument();
|
||||
expect(screen.getByText('Regional')).toBeInTheDocument();
|
||||
expect(screen.getByText('0x1234, 0x5678 · unknown region')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the resolved region name in the scope card when the backend matched one', () => {
|
||||
render(
|
||||
<RawPacketDetailModal
|
||||
packet={{ ...SCOPED_PACKET, region: 'nl-gr', transport_code: 0x1234 }}
|
||||
channels={[]}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Scope')).toBeInTheDocument();
|
||||
expect(screen.getByText('nl-gr')).toBeInTheDocument();
|
||||
// Raw codes remain visible as the secondary detail.
|
||||
expect(screen.getByText('0x1234, 0x5678')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
RENDER_RICH_PAYLOADS_KEY,
|
||||
getSavedRenderRichPayloads,
|
||||
setSavedRenderRichPayloads,
|
||||
} from '../utils/richPayloadPreference';
|
||||
|
||||
describe('richPayloadPreference utilities', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('defaults to off when unset', () => {
|
||||
expect(getSavedRenderRichPayloads()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when enabled', () => {
|
||||
localStorage.setItem(RENDER_RICH_PAYLOADS_KEY, 'true');
|
||||
expect(getSavedRenderRichPayloads()).toBe(true);
|
||||
});
|
||||
|
||||
it('treats any non-"true" value as off', () => {
|
||||
localStorage.setItem(RENDER_RICH_PAYLOADS_KEY, 'yes');
|
||||
expect(getSavedRenderRichPayloads()).toBe(false);
|
||||
});
|
||||
|
||||
it('persists when enabled and clears the key when disabled', () => {
|
||||
setSavedRenderRichPayloads(true);
|
||||
expect(localStorage.getItem(RENDER_RICH_PAYLOADS_KEY)).toBe('true');
|
||||
|
||||
setSavedRenderRichPayloads(false);
|
||||
expect(localStorage.getItem(RENDER_RICH_PAYLOADS_KEY)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -66,6 +66,7 @@ const baseSettings: AppSettings = {
|
||||
advert_interval: 0,
|
||||
last_advert_time: 0,
|
||||
flood_scope: '',
|
||||
known_regions: [],
|
||||
blocked_keys: [],
|
||||
blocked_names: [],
|
||||
discovery_blocked_types: [],
|
||||
|
||||
Reference in New Issue
Block a user