Compare commits
4
Commits
013f113bc2
...
a3ca951fba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3ca951fba | ||
|
|
33cb103abb | ||
|
|
7ad948e26c | ||
|
|
07b0c410ab |
+61
-1
@@ -706,8 +706,9 @@ KaTeX-rendered math in messages, two paths:
|
||||
|
||||
- **Spec path (CS-API §11.5):** `<span/div data-mx-maths="…">` in `formatted_body` renders the attribute's LaTeX (block for div, inline for span); on render failure the element's child fallback content shows instead
|
||||
- **Plain-text path:** `$…$` (inline) and `$$…$$` (block) with conservative rules — escape-aware (`\$`), currency-guarded (`$5 and $10` stays text), never inside `code`/`pre`
|
||||
- **Outgoing interop:** on send, the composer converts `$…$`/`$$…$$` to spec `<span/div data-mx-maths>` HTML in `formatted_body` (extracted before markdown so LaTeX isn't mangled; off inside code), so math renders on Element and every other client — not just Lotus. The plain `body` keeps literal `$…$` as the fallback
|
||||
- KaTeX + its CSS load lazily on first math encountered — zero cost to the main bundle
|
||||
- Files: `src/app/utils/mathParse.ts` (+14 tests), `components/math/KaTeX.tsx`, `plugins/react-custom-html-parser.tsx`
|
||||
- Files: `src/app/utils/mathParse.ts` (+14 tests), `components/math/KaTeX.tsx`, `plugins/react-custom-html-parser.tsx` (render), `components/editor/output.ts` (+ `output.test.ts`, outgoing)
|
||||
|
||||
### Image / Video Captions
|
||||
|
||||
@@ -767,6 +768,65 @@ Redacted events display "This message has been deleted" along with the redaction
|
||||
|
||||
Generic (non-domain-specific) cards display a Google S2 favicon. Empty or unparseable preview responses are suppressed entirely rather than showing a blank card.
|
||||
|
||||
### Inline Media Embeds
|
||||
|
||||
Media links play/render **in place** instead of opening a browser tab. A pure
|
||||
resolver, `parseMediaEmbed(url, host)` in `src/app/utils/videoEmbed.ts`, maps a
|
||||
URL to `{ provider, kind, embedUrl }`; `MediaEmbedCard` / `TikTokEmbedCard` /
|
||||
`TwitterCard` in `UrlPreviewCard.tsx` render it. Four render `kind`s:
|
||||
|
||||
| kind | shape | providers |
|
||||
| ----------- | ------------------------ | ----------------------------------------------------------------- |
|
||||
| `landscape` | 16:9 video player | YouTube, Vimeo, Dailymotion, Streamable, Twitch, Loom, Kick (live) |
|
||||
| `portrait` | 9:16 video player | YouTube Shorts, TikTok |
|
||||
| `audio` | fixed-height audio player | Spotify, SoundCloud, Apple Music, Tidal |
|
||||
| `rich` | self-resizing post embed | X/Twitter, Instagram, Reddit, Bluesky |
|
||||
|
||||
**Privacy-friendly facade.** The tile first shows the homeserver's cached
|
||||
`og:image` thumbnail + a play button; the third-party `<iframe>` is only mounted
|
||||
on click, so nothing hits Google/Meta/etc. until the user opts in. A **Close**
|
||||
button collapses a playing embed back to the facade, and video players carry a
|
||||
Fullscreen control. Cookie-less/DNT variants are used where offered
|
||||
(`youtube-nocookie.com`, Vimeo `dnt=1`).
|
||||
|
||||
**Provider notes.**
|
||||
|
||||
- **TikTok** — short "copy-link" URLs (`vm.tiktok.com`, `tiktok.com/t/…`) carry no
|
||||
video id and the homeserver preview is bot-walled, so `TikTokEmbedCard`
|
||||
resolves the id client-side via TikTok's CORS-enabled **oEmbed** API on click
|
||||
(`AbortController`-guarded), then plays the `player/v1` embed.
|
||||
- **Reddit / Instagram / Bluesky / X** — post embeds self-size via `postMessage`;
|
||||
`useIframeAutoHeight` listens scoped to each provider's origin **and** our own
|
||||
iframe, parsing each provider's height shape (Instagram `MEASURE`, Reddit
|
||||
`resize.embed`, Twitter `twttr.private.resize`). `redd.it` short links resolve to
|
||||
the subreddit-less `embed.reddit.com/comments/{id}/` route.
|
||||
- **Vimeo** unlisted-video privacy hashes (`vimeo.com/{id}/{hash}`) and
|
||||
channel/group/album forms are parsed; **YouTube** handles `/watch`, `youtu.be`,
|
||||
`/embed`, `/live`, `/shorts`, and `m.`/`music.youtube.com`.
|
||||
|
||||
**Defense-in-depth.** Every embed iframe carries a `sandbox` that omits
|
||||
`allow-top-navigation` (so a compromised embed can't redirect the whole app —
|
||||
phishing guard) on top of the CSP `frame-src` allowlist. Previews are also capped
|
||||
at 6 per message.
|
||||
|
||||
**Setting.** `inlineMediaEmbeds` (Settings → General → "Inline Media Players",
|
||||
default **on**). Off → media links fall back to plain link tiles.
|
||||
|
||||
**Latent web bug fixed along the way.** YouTube thumbnails now come from the
|
||||
homeserver `og:image` instead of `img.youtube.com` — which was silently broken on
|
||||
the web build (nginx `img-src` has no YouTube host) — removing a pre-click Google
|
||||
request as a bonus.
|
||||
|
||||
**CSP.** Desktop Tauri `frame-src` (`cinny-desktop` `tauri.conf.json`) and the web
|
||||
nginx `frame-src` allowlist enumerate every embed host (youtube-nocookie,
|
||||
player.vimeo, geo.dailymotion, streamable, player/clips.twitch, open.spotify,
|
||||
w.soundcloud, embed.music.apple, embed.tidal, www.tiktok + connect-src for its
|
||||
oEmbed, platform.twitter, www.instagram, embed.reddit, embed.bsky.app, www.loom,
|
||||
player.kick).
|
||||
|
||||
**Files:** `src/app/utils/videoEmbed.ts` (resolver + parsers, unit-tested),
|
||||
`src/app/components/url-preview/{UrlPreviewCard,UrlPreview.css}.tsx`.
|
||||
|
||||
### Poll Creation
|
||||
|
||||
- `PollCreator.tsx` creates stable `m.poll.start` events
|
||||
|
||||
@@ -662,6 +662,43 @@ Run the axe DevTools extension (or Lighthouse → Accessibility) on a room view,
|
||||
|
||||
---
|
||||
|
||||
## Q. Inline Media Embeds — video / audio / post players (needs the web deploy live)
|
||||
|
||||
The whole feature is behind **Settings → General → "Inline Media Players"** (default **on**). Everything loads from the homeserver's cached thumbnail first; the third-party player only mounts on **Play**. Test on the **web** build first, then re-check the video ones on **desktop (Tauri)** since the CSP differs. On any failure, grab the **browser console** (F12) — a blocked embed shows as a CSP `frame-src` violation naming the host.
|
||||
|
||||
### Q1. Facade + one of each kind plays in place
|
||||
|
||||
Paste each of these into a room and confirm a media tile (not a plain link) with a thumbnail + play button, and that clicking Play mounts the player **inline**:
|
||||
|
||||
- **16:9 video:** a YouTube `watch` link, a Vimeo link, a Dailymotion link, a Streamable link, a Twitch VOD/clip, a Loom `share` link.
|
||||
- **9:16 portrait:** a YouTube **Shorts** link (renders tall, not letterboxed).
|
||||
- **Audio player:** a Spotify track, a SoundCloud track, an Apple Music album, a Tidal album/track.
|
||||
- **Post embed:** an X/Twitter post, an Instagram post, a Reddit post.
|
||||
|
||||
**Expected:** ✅ tile shows the thumbnail; **no** request to the third party until you press Play (check DevTools → Network); the player then plays inline. ❌ tell me any that stay a plain link, show a blank frame, or hit the network before you click.
|
||||
|
||||
### Q2. TikTok (the tricky one) + portrait fill
|
||||
|
||||
1. Paste a **full** TikTok URL and a **short** copy-link (`vm.tiktok.com/…` or `tiktok.com/t/…`).
|
||||
2. Press Play on each.
|
||||
|
||||
**Expected:** both resolve to a clean **9:16** player that **fills the box** (no big empty band on the right). The short link shows a brief spinner while it resolves via oEmbed, then plays. ❌ tell me if a short link shows only the TikTok logo/♫ and never a play button, or if the player has dead space beside it.
|
||||
|
||||
### Q3. Post self-resize + Close / Fullscreen controls
|
||||
|
||||
1. Play a **Reddit**, **Instagram**, and **X/Twitter** post embed.
|
||||
2. Watch the card height as the embed loads.
|
||||
|
||||
**Expected:** the card **grows to fit** the post (no clipped/scrollbarless content, no giant empty box). A **Close** button (✕) collapses the player back to the thumbnail; video players also show a **⛶ Fullscreen** control that works. Keyboard: Tab to the play button → it shows a visible **focus ring**.
|
||||
|
||||
### Q4. New providers (unverified) + the toggle + the cap
|
||||
|
||||
- **Bluesky / Loom / Kick** — these are freshly added and unverified live. Paste a `bsky.app/profile/…/post/…`, a `loom.com/share/…`, and a live `kick.com/{channel}` link. ✅ good if each plays/renders inline; ❌ if any is a broken frame (for **Bluesky** especially, note whether a **handle** URL resolves or only a DID one does — grab the console).
|
||||
- **Toggle off:** Settings → General → **Inline Media Players** off → every media link reverts to a plain link tile (no player).
|
||||
- **Cap:** paste a message with **8+** media links → at most **6** preview cards render (the rest are suppressed), and the page stays responsive.
|
||||
|
||||
---
|
||||
|
||||
## Priority if you're short on time
|
||||
|
||||
1. **O1 + O2** (threads + per-thread notifications) — the largest new surface; the main-timeline change is user-visible.
|
||||
|
||||
+19
-2
@@ -57,6 +57,7 @@ Built and gate-green; verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then gr
|
||||
| Desktop proactive update notifications (P5-40) | J1 |
|
||||
| OIDC/SSO login (P4-6, needs an MSC3861 server — pick mozilla.org on login) | OIDC |
|
||||
| Windows native WinRT toast quick-reply / click-to-open (D6, AUMID) | rich-toast (§backlog) |
|
||||
| Inline media embeds (16 providers: video/audio/post + click-to-play facade) | Q1 / Q2 / Q3 / Q4 |
|
||||
|
||||
---
|
||||
|
||||
@@ -152,9 +153,15 @@ After Phases A–C the client spec is ~complete. What's left, flagged by **what
|
||||
|
||||
A minimal audio editor for soundboard clips and voice content. Scope: (1) **trim/clip** an audio file to a chosen start/end (waveform scrubber, in/out handles); (2) **upload a video file → strip and discard the video track, keep only the audio** (extract audio, then the source video is dropped — never uploaded/stored); (3) minimal edits only (trim, maybe gain/normalize, fade in/out) — not a full DAW. Likely Web Audio API (`AudioContext.decodeAudioData` → trim `AudioBuffer` → re-encode) + `MediaRecorder`/an encoder for output; video demux via a `<video>`+`MediaElementSource` capture or ffmpeg.wasm (weigh bundle cost). Feeds the soundboard uploader (`utils/soundboardClips.ts`, `SoundboardPackEditor`) and attachments. Design under TDS + native-cinny law. Big build — plan a dedicated session; evaluate ffmpeg.wasm size/CSP (wasm) before committing.
|
||||
|
||||
### [ ] P4-4 · Math / LaTeX Rendering (LOW PRIORITY)
|
||||
### [x] P4-4 · Math / LaTeX Rendering — DONE
|
||||
|
||||
Render `$…$` / `$$…$$` via KaTeX; graceful fallback to raw text. **Sanitizer must be patched** — `src/app/utils/sanitize.ts` (sanitize-html, `disallowedTagsMode:'discard'`) strips all MathML: add `<math><mi><mo><mn><mrow><mfrac><msqrt><mroot><msub><msup><msubsup><munder><mover><mtable><mtr><mtd>…` + `annotation` to `permittedHtmlTags`, and `xmlns`/`display`/`mathvariant` to `permittedTagToAttributes`. Parser: split text nodes on `/(\$\$.*?\$\$|\$.*?\$)/g` in `react-custom-html-parser.tsx` → `<KaTeX>`. Lazy-import `katex/dist/katex.min.css` only when a math block renders. Verify KaTeX bundle-size impact.
|
||||
Rendering shipped (KaTeX, `$…$`/`$$…$$` + spec `data-mx-maths`, lazy-loaded,
|
||||
`<pre>/<code>`-guarded) — see LOTUS_FEATURES.md. **Outgoing cross-client interop
|
||||
added (2026-07):** the composer now emits spec `data-mx-maths` HTML on send
|
||||
(`editor/output.ts`, reusing `splitMathSegments`), so math a Lotus user types
|
||||
renders on Element and every other client, not just Lotus. Deferred: multi-line
|
||||
block `$$…$$` (spans editor paragraph nodes) still renders on Lotus via the
|
||||
plain-body `$…$` path only.
|
||||
|
||||
### [~] P5-20 · Quick Reply from Browser Notification (partial)
|
||||
|
||||
@@ -172,6 +179,16 @@ Phase 1 shipped: `io.lotus.set_deafen` (LiveKit-source deafen/screenshare-audio-
|
||||
|
||||
Comprehensive audit of all LOTUS_FEATURES.md features for mobile PWA usability + responsiveness. Method: 44px touch targets, no horizontal overflow, full-screen modals/drawers on mobile, composer not obscured by keyboard.
|
||||
|
||||
### [ ] Inline media embeds — remaining providers (LOW PRIORITY)
|
||||
|
||||
The inline embed system (`videoEmbed.ts`) covers 16 providers; three more were **deliberately deferred** (verified against 2026 docs by review agents):
|
||||
|
||||
- **Bandcamp** (highest-value audio add) — needs an **oEmbed** round-trip: the player URL requires numeric `album`/`track` item ids that aren't in the page URL (`bandcamp.com/oembed` is the resolver; mirror the `TikTokEmbedCard` on-click oEmbed pattern). CSP `frame-src`: `bandcamp.com`. Classify `kind: 'audio'`.
|
||||
- **SoundCloud `on.soundcloud.com` short links** — the `w.soundcloud` widget resolver does **not** follow the redirect; needs the same on-click oEmbed resolve (`soundcloud.com/oembed`, CORS-enabled) to get the canonical URL. (Canonical `soundcloud.com/{user}/{track}` links already work.)
|
||||
- **Vimeo `event/{id}` (live events) + `ondemand/…`** — event embed host is `vimeo.com` (**not** `player.vimeo.com`, so it needs a new CSP `frame-src` host); on-demand is paywalled and doesn't embed for non-purchasers. Low ROI — only do the event case if `vimeo.com` is widened for another reason.
|
||||
|
||||
Also open (from the quality review): a real `onError`/error-state fallback for iframes that fail to load (deleted post / region lock / X login-wall) — cross-origin frames don't fire `onError` reliably, so this needs a load-timeout heuristic; the Close button + badge link are the current escape hatch.
|
||||
|
||||
### Deferred / dropped (decided — kept for context)
|
||||
|
||||
- **[DEFERRED] P5-51** Federated "Identity Contexts" (session isolation) — multi-sprint, touches auth/crypto/storage core; smaller intermediate step = plain multi-account switch. **[DROPPED] P5-52** per-room sync governor — js-sdk can't truly per-room filter `/sync`; only a cosmetic hide. **[DEFERRED] P5-53** local scripting plugin — prefer a declarative automation-rules feature (no arbitrary code). **[DEFERRED] Audit-3** profile banner — MSC4427 open/unmerged; revisit on merge. **[WON'T FIX] P5-50** Windows HW media pipeline (WebRTC decode lives in WebView2; not injectable). **[MOVED] P5-9** LFG → LotusBot `!lfg`.
|
||||
|
||||
@@ -41,6 +41,7 @@ The Lotus Chat logo (`public/res/Lotus.png`) is a derivative work based on the o
|
||||
- Deleted messages show a placeholder instead of disappearing
|
||||
- Code blocks highlight syntax for JS/TS, Python, and Rust
|
||||
- Rich link preview cards for YouTube, GitHub, Twitter/X, Reddit, Spotify, Twitch, Steam, Wikipedia, Discord, npm, Stack Overflow, and IMDb
|
||||
- Inline media embeds — play videos and posts in place instead of opening a browser tab: YouTube/Shorts, Vimeo, Dailymotion, Streamable, Twitch, Loom, and Kick as video players; TikTok, X/Twitter, Instagram, Reddit, and Bluesky as inline posts; Spotify, SoundCloud, Apple Music, and Tidal as a built-in audio player. A privacy-friendly facade shows the homeserver's cached thumbnail and only loads the third-party player when you press play. Toggle at Settings → General → "Inline Media Players" (on by default)
|
||||
|
||||
### Calls & Voice
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Descendant } from 'slate';
|
||||
import { toMatrixCustomHTML } from './output';
|
||||
import { BlockType } from './types';
|
||||
|
||||
// Loose Slate node builders for the test.
|
||||
const txt = (text: string, marks: Record<string, unknown> = {}) =>
|
||||
({ text, ...marks }) as unknown as Descendant;
|
||||
const el = (type: BlockType, children: unknown[]) =>
|
||||
({ type, children }) as unknown as Descendant;
|
||||
|
||||
const OPTS = {
|
||||
allowTextFormatting: true,
|
||||
allowInlineMarkdown: false,
|
||||
allowBlockMarkdown: false,
|
||||
allowMath: true,
|
||||
};
|
||||
|
||||
test('inline $…$ → data-mx-maths span, surrounding prose preserved', () => {
|
||||
assert.equal(
|
||||
toMatrixCustomHTML(txt('a $x^2$ b'), OPTS),
|
||||
'a <span data-mx-maths="x^2"><code>x^2</code></span> b',
|
||||
);
|
||||
});
|
||||
|
||||
test('single-line block $$…$$ → data-mx-maths div', () => {
|
||||
assert.equal(
|
||||
toMatrixCustomHTML(txt('$$E=mc^2$$'), OPTS),
|
||||
'<div data-mx-maths="E=mc^2"><code>E=mc^2</code></div>',
|
||||
);
|
||||
});
|
||||
|
||||
test('LaTeX special chars are escaped in attribute + fallback', () => {
|
||||
assert.equal(
|
||||
toMatrixCustomHTML(txt('$a<b$'), OPTS),
|
||||
'<span data-mx-maths="a<b"><code>a<b</code></span>',
|
||||
);
|
||||
});
|
||||
|
||||
test('math bypasses markdown — underscores are not italicised', () => {
|
||||
// Without pre-markdown extraction, `_` would become <em>.
|
||||
assert.equal(
|
||||
toMatrixCustomHTML(txt('$a_b$'), { ...OPTS, allowInlineMarkdown: true }),
|
||||
'<span data-mx-maths="a_b"><code>a_b</code></span>',
|
||||
);
|
||||
});
|
||||
|
||||
test('currency ($5 and $10) stays literal', () => {
|
||||
assert.equal(toMatrixCustomHTML(txt('$5 and $10'), OPTS), '$5 and $10');
|
||||
});
|
||||
|
||||
test('no math conversion inside an inline code mark', () => {
|
||||
const out = toMatrixCustomHTML(txt('$x$', { code: true }), OPTS);
|
||||
assert.ok(!out.includes('data-mx-maths'));
|
||||
assert.ok(out.includes('<code>$x$</code>'));
|
||||
});
|
||||
|
||||
test('no math conversion inside a code block', () => {
|
||||
const block = el(BlockType.CodeBlock, [el(BlockType.CodeLine, [txt('$x$')])]);
|
||||
const out = toMatrixCustomHTML(block, OPTS);
|
||||
assert.ok(!out.includes('data-mx-maths'));
|
||||
assert.ok(out.includes('$x$'));
|
||||
});
|
||||
|
||||
test('a message with no math is unchanged', () => {
|
||||
assert.equal(toMatrixCustomHTML(txt('just hello'), OPTS), 'just hello');
|
||||
});
|
||||
@@ -12,14 +12,42 @@ import {
|
||||
import { findAndReplace } from '../../utils/findAndReplace';
|
||||
import { sanitizeForRegex } from '../../utils/regex';
|
||||
import { isUserId } from '../../utils/matrix';
|
||||
import { splitMathSegments } from '../../utils/mathParse';
|
||||
|
||||
export type OutputOptions = {
|
||||
allowTextFormatting?: boolean;
|
||||
allowInlineMarkdown?: boolean;
|
||||
allowBlockMarkdown?: boolean;
|
||||
allowMath?: boolean;
|
||||
};
|
||||
|
||||
// Spec `data-mx-maths` markup (CS-API §11.5): the attribute holds the LaTeX; the
|
||||
// <code> child is the fallback for clients that don't render math. sanitizeText
|
||||
// escapes & < > " ' — correct for both the attribute value and the <code> text.
|
||||
const mathToCustomHtml = (latex: string, block: boolean): string => {
|
||||
const esc = sanitizeText(latex);
|
||||
const tag = block ? 'div' : 'span';
|
||||
return `<${tag} data-mx-maths="${esc}"><code>${esc}</code></${tag}>`;
|
||||
};
|
||||
|
||||
const textToCustomHtml = (node: Text, opts: OutputOptions): string => {
|
||||
// Convert `$…$`/`$$…$$` to `data-mx-maths` so math renders on other clients.
|
||||
// Extracted BEFORE markdown so LaTeX (`_`, `*`, `\`, `{}`) isn't mangled; never
|
||||
// applied inside inline code. Non-math text recurses with allowMath off so it
|
||||
// still gets the normal marks + inline-markdown treatment.
|
||||
if (opts.allowMath && !node.code) {
|
||||
const segments = splitMathSegments(node.text);
|
||||
if (segments.some((seg) => seg.type !== 'text')) {
|
||||
return segments
|
||||
.map((seg) =>
|
||||
seg.type === 'text'
|
||||
? textToCustomHtml({ ...node, text: seg.value }, { ...opts, allowMath: false })
|
||||
: mathToCustomHtml(seg.value, seg.type === 'block')
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
let string = sanitizeText(node.text);
|
||||
if (opts.allowTextFormatting) {
|
||||
if (node.bold) string = `<strong>${string}</strong>`;
|
||||
|
||||
@@ -514,6 +514,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
allowTextFormatting: true,
|
||||
allowBlockMarkdown: isMarkdown,
|
||||
allowInlineMarkdown: isMarkdown,
|
||||
allowMath: true,
|
||||
}),
|
||||
);
|
||||
let msgType = MsgType.Text;
|
||||
@@ -616,6 +617,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
allowTextFormatting: true,
|
||||
allowBlockMarkdown: isMarkdown,
|
||||
allowInlineMarkdown: isMarkdown,
|
||||
allowMath: true,
|
||||
}),
|
||||
);
|
||||
if (plainText === '') return null;
|
||||
|
||||
@@ -117,6 +117,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||
allowTextFormatting: true,
|
||||
allowBlockMarkdown: isMarkdown,
|
||||
allowInlineMarkdown: isMarkdown,
|
||||
allowMath: true,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -357,6 +357,28 @@ test('isNotificationEvent accepts message/sticker but rejects member, redacted,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
// Device-verification requests (m.room.message + this msgtype) are crypto
|
||||
// control messages — must NOT count as unread/notify (stale-request nag fix).
|
||||
assert.equal(
|
||||
isNotificationEvent(
|
||||
mockEvent({
|
||||
getType: () => 'm.room.message',
|
||||
getContent: () => ({ msgtype: 'm.key.verification.request' }),
|
||||
}),
|
||||
),
|
||||
false,
|
||||
);
|
||||
// Regression guard: a normal text message still notifies.
|
||||
assert.equal(
|
||||
isNotificationEvent(
|
||||
mockEvent({
|
||||
getType: () => 'm.room.message',
|
||||
getContent: () => ({ msgtype: 'm.text', body: 'hi' }),
|
||||
}),
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// --- roomHaveNotification / getUnreadInfo --------------------------------
|
||||
|
||||
@@ -195,12 +195,21 @@ const NOTIFICATION_EVENT_TYPES = [
|
||||
'm.room.member',
|
||||
'm.sticker',
|
||||
];
|
||||
// In-room device-verification requests are sent as m.room.message with this
|
||||
// msgtype (the rest of the flow — start/accept/key/mac/done/cancel — uses its own
|
||||
// event types, already excluded above). They're crypto control messages, not chat.
|
||||
const VERIFICATION_REQUEST_MSGTYPE = 'm.key.verification.request';
|
||||
export const isNotificationEvent = (mEvent: MatrixEvent) => {
|
||||
const eType = mEvent.getType();
|
||||
if (!NOTIFICATION_EVENT_TYPES.includes(eType)) {
|
||||
return false;
|
||||
}
|
||||
if (eType === 'm.room.member') return false;
|
||||
// Don't badge/notify a verification request — otherwise a stale one at the tail
|
||||
// of a DM re-lights the room's unread dot on every fresh sync (cache clear).
|
||||
if (eType === 'm.room.message' && mEvent.getContent().msgtype === VERIFICATION_REQUEST_MSGTYPE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mEvent.isRedacted()) return false;
|
||||
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
|
||||
|
||||
Reference in New Issue
Block a user