Feat/messaging ui foundation #13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@insignia/iios-messaging-ui",
|
"name": "@insignia/iios-messaging-ui",
|
||||||
"version": "0.1.9",
|
"version": "0.1.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Conversation } from '../types';
|
import type { Conversation } from '../types';
|
||||||
|
import { stripMarkup } from '../rich-text';
|
||||||
|
|
||||||
/** Initials for the avatar chip — first letters of the first two words. */
|
/** Initials for the avatar chip — first letters of the first two words. */
|
||||||
function initials(title: string): string {
|
function initials(title: string): string {
|
||||||
@@ -37,7 +38,7 @@ export function ConversationList({
|
|||||||
</span>
|
</span>
|
||||||
<span className="miu-convrow-main">
|
<span className="miu-convrow-main">
|
||||||
<span className="miu-convrow-title">{c.title}</span>
|
<span className="miu-convrow-title">{c.title}</span>
|
||||||
{c.lastMessage ? <span className="miu-convrow-preview">{c.lastMessage}</span> : null}
|
{c.lastMessage ? <span className="miu-convrow-preview">{stripMarkup(c.lastMessage)}</span> : null}
|
||||||
</span>
|
</span>
|
||||||
{c.unread > 0 ? (
|
{c.unread > 0 ? (
|
||||||
<span className="miu-badge" aria-label={`${c.unread} unread`}>
|
<span className="miu-badge" aria-label={`${c.unread} unread`}>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export { useChannels } from './hooks/use-channels';
|
|||||||
export { useMembers } from './hooks/use-members';
|
export { useMembers } from './hooks/use-members';
|
||||||
export { isOwnMessage } from './types';
|
export { isOwnMessage } from './types';
|
||||||
// Message-body markup (bold/italic/strike/code/links + mentions) as a safe ReactNode tree.
|
// Message-body markup (bold/italic/strike/code/links + mentions) as a safe ReactNode tree.
|
||||||
export { renderRichText, safeHref } from './rich-text';
|
export { renderRichText, safeHref, stripMarkup } from './rich-text';
|
||||||
|
|
||||||
// Rendered UI. Pair with the './styles.css' export (or override the --miu-* tokens).
|
// Rendered UI. Pair with the './styles.css' export (or override the --miu-* tokens).
|
||||||
export { Messenger } from './components/messenger';
|
export { Messenger } from './components/messenger';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { renderRichText, safeHref } from './rich-text';
|
import { renderRichText, safeHref, stripMarkup } from './rich-text';
|
||||||
|
|
||||||
function show(text: string, names: string[] = []) {
|
function show(text: string, names: string[] = []) {
|
||||||
render(<div data-testid="out">{renderRichText(text, names)}</div>);
|
render(<div data-testid="out">{renderRichText(text, names)}</div>);
|
||||||
@@ -75,3 +75,21 @@ describe('renderRichText', () => {
|
|||||||
expect(show('just a normal message').textContent).toBe('just a normal message');
|
expect(show('just a normal message').textContent).toBe('just a normal message');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('stripMarkup (one-line previews)', () => {
|
||||||
|
it('drops the markers so a preview never shows ~crazy~', () => {
|
||||||
|
expect(stripMarkup('~crazy~')).toBe('crazy');
|
||||||
|
expect(stripMarkup('*bold* and _italic_')).toBe('bold and italic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unwraps code and flattens a fenced block', () => {
|
||||||
|
expect(stripMarkup('run `npm ci` now')).toBe('run npm ci now');
|
||||||
|
expect(stripMarkup('```\nconst a = 1;\n```')).toBe('const a = 1;');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles nesting and leaves ordinary text (and identifiers) alone', () => {
|
||||||
|
expect(stripMarkup('*bold with _italic_*')).toBe('bold with italic');
|
||||||
|
expect(stripMarkup('snake_case_name and 5 * 3')).toBe('snake_case_name and 5 * 3');
|
||||||
|
expect(stripMarkup('plain preview')).toBe('plain preview');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -71,6 +71,25 @@ function firstSpan(text: string): { start: number; end: number; rule: Rule; inne
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same text with its formatting markers removed, for places that show a one-line plain-text
|
||||||
|
* preview (conversation list, notifications) where `~crazy~` must read as "crazy" — you cannot
|
||||||
|
* render nodes into those, so the markers have to come off rather than be styled.
|
||||||
|
*/
|
||||||
|
export function stripMarkup(text: string): string {
|
||||||
|
const noCode = text
|
||||||
|
.replace(/```([\s\S]*?)```/g, (_m, code: string) => code.trim())
|
||||||
|
.replace(/`([^`\n]+)`/g, '$1');
|
||||||
|
return stripSpans(noCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recursively drop *bold* / _italic_ / ~strike~ markers, honouring the same word-boundary rule. */
|
||||||
|
function stripSpans(text: string): string {
|
||||||
|
const span = firstSpan(text);
|
||||||
|
if (!span) return text;
|
||||||
|
return text.slice(0, span.start) + stripSpans(span.inner) + stripSpans(text.slice(span.end));
|
||||||
|
}
|
||||||
|
|
||||||
/** Linkify + mention-highlight a run of text that carries no other markup. */
|
/** Linkify + mention-highlight a run of text that carries no other markup. */
|
||||||
function plain(text: string, memberNames: string[], keyed: () => string): ReactNode[] {
|
function plain(text: string, memberNames: string[], keyed: () => string): ReactNode[] {
|
||||||
const out: ReactNode[] = [];
|
const out: ReactNode[] = [];
|
||||||
|
|||||||
@@ -568,8 +568,14 @@ button.miu-attach-dl:hover {
|
|||||||
color: var(--miu-accent);
|
color: var(--miu-accent);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.miu-msg.is-mine .miu-bubble .miu-link,
|
.miu-msg.is-mine .miu-bubble .miu-link {
|
||||||
.miu-msg.is-mine .miu-bubble .miu-code {
|
color: var(--miu-accent-text);
|
||||||
|
}
|
||||||
|
/* On your own (accent-filled) bubble the panel background would sit dark-on-dark against the dark
|
||||||
|
accent text — tint the bubble instead so the chip reads on any accent colour. */
|
||||||
|
.miu-msg.is-mine .miu-bubble .miu-code,
|
||||||
|
.miu-msg.is-mine .miu-bubble .miu-code-block {
|
||||||
|
background: rgba(0, 0, 0, 0.16);
|
||||||
color: var(--miu-accent-text);
|
color: var(--miu-accent-text);
|
||||||
}
|
}
|
||||||
.miu-send {
|
.miu-send {
|
||||||
|
|||||||
Reference in New Issue
Block a user