mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
feat: improved Relay Chat with clickable Nomad and LXMF links, and support for basic markdown formatting in messages
This commit is contained in:
parent
67e78aa299
commit
38a09762fc
6 changed files with 126 additions and 3 deletions
|
|
@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Messages page conversation poll is slower and skips while the tab is hidden
|
||||
- Schema v51 message-flag backfill skips empty databases so fresh init stays fast
|
||||
- Relay Chat: denser hub UI, announce interval, collapsed system lines, reconnect notices
|
||||
- Relay Chat: clickable Nomad and LXMF links plus basic markdown for code, bold, italic, and strikethrough
|
||||
- Low-memory cleanup and SQLite pragmas under memory pressure
|
||||
- CI benches use median-of-medians and quieter regression gates
|
||||
- Backend benchmarks cover slim conversation list, mark-as-read, call history, and missed-call notification paths
|
||||
|
|
|
|||
BIN
meshchatx.rsm
BIN
meshchatx.rsm
Binary file not shown.
|
|
@ -1325,6 +1325,8 @@ import { loadRelayLayout, saveRelayLayout } from "../../js/relayLayoutStore.js";
|
|||
import { loadFeatureSidebarCollapsed, saveFeatureSidebarCollapsed } from "../../js/browserLayoutStore.js";
|
||||
import { RELAY_HOST_MODAL_OVERLAY, RELAY_HOST_MODAL_PANEL_COMPACT } from "../../js/relayHostModalClasses.js";
|
||||
import { buildRelayShareMessage } from "../../js/relayLinkUtils.js";
|
||||
import { handleRichHtmlLinkClick } from "../../js/NomadRichHtmlLinks.js";
|
||||
import MarkdownRenderer from "../../js/MarkdownRenderer.js";
|
||||
import {
|
||||
ANNOUNCE_SLIDER_POS_MAX,
|
||||
announceMinutesToSliderPos,
|
||||
|
|
@ -2407,6 +2409,33 @@ export default {
|
|||
return "";
|
||||
}
|
||||
},
|
||||
renderMessageHtml(text) {
|
||||
return MarkdownRenderer.renderBasic(text);
|
||||
},
|
||||
handleMessageHtmlClick(event) {
|
||||
const hex32 = /^[a-fA-F0-9]{32}$/;
|
||||
const routeName = this.$route?.meta?.isPopout ? "nomadnetwork-popout" : "nomadnetwork";
|
||||
handleRichHtmlLinkClick(event, {
|
||||
onNomadUrl: (url) => {
|
||||
const [hash, ...pathParts] = url.split(":");
|
||||
const path = pathParts.join(":");
|
||||
if (!hex32.test(hash)) {
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
name: routeName,
|
||||
params: { destinationHash: hash },
|
||||
query: { path },
|
||||
});
|
||||
},
|
||||
onLxmfAddress: (address) => {
|
||||
this.$router.push({
|
||||
name: "messages",
|
||||
params: { destinationHash: address },
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
formatHash(hash) {
|
||||
if (!hash) {
|
||||
return "-";
|
||||
|
|
|
|||
|
|
@ -54,7 +54,12 @@
|
|||
:data-msg-key="page.messageKey(entry.msg)"
|
||||
>
|
||||
<span class="mr-1 text-xs text-sem-fg-muted">{{ page.formatTime(entry.msg.ts) }}</span>
|
||||
* {{ page.displayName(entry.msg) }} {{ entry.msg.text }}
|
||||
* {{ page.displayName(entry.msg) }}
|
||||
<span
|
||||
class="break-words"
|
||||
v-html="page.renderMessageHtml(entry.msg.text)"
|
||||
@click="page.handleMessageHtmlClick($event)"
|
||||
></span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="entry.msg"
|
||||
|
|
@ -65,7 +70,11 @@
|
|||
>
|
||||
<span class="mr-1.5 text-xs text-sem-fg-muted">{{ page.formatTime(entry.msg.ts) }}</span>
|
||||
<span class="mr-1.5 font-semibold" :style="page.nameStyle(entry.msg)">{{ page.displayName(entry.msg) }}:</span>
|
||||
<span class="whitespace-pre-wrap break-words">{{ entry.msg.text }}</span>
|
||||
<span
|
||||
class="whitespace-pre-wrap break-words"
|
||||
v-html="page.renderMessageHtml(entry.msg.text)"
|
||||
@click="page.handleMessageHtmlClick($event)"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,48 @@ export default class MarkdownRenderer {
|
|||
return processed_parts.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal markdown for chat surfaces (RRC): links, inline code, bold, italic, strikethrough.
|
||||
* Links run before inline code so Nomad field data after a single backtick stays intact.
|
||||
*/
|
||||
static renderBasic(text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof text !== "string") {
|
||||
text = String(text);
|
||||
}
|
||||
|
||||
text = Utils.escapeHtml(text);
|
||||
text = LinkUtils.renderAllLinks(text);
|
||||
|
||||
const { protectedText, anchors } = LinkUtils.protectAnchors(text);
|
||||
text = protectedText;
|
||||
|
||||
const inline_codes = [];
|
||||
const pushInline = (code) => {
|
||||
const placeholder = `[[IC${inline_codes.length}]]`;
|
||||
inline_codes.push(
|
||||
`<code class="bg-black/10 dark:bg-white/10 px-1 rounded-sm font-mono text-[0.9em]">${code}</code>`
|
||||
);
|
||||
return placeholder;
|
||||
};
|
||||
text = text.replace(/``([^`]+)``/g, (_m, code) => pushInline(code));
|
||||
text = text.replace(/`([^`]+)`/g, (_m, code) => pushInline(code));
|
||||
|
||||
text = text.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
||||
text = text.replace(/\*\*\*(.*?)\*\*\*/g, "<strong><em>$1</em></strong>");
|
||||
text = text.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
text = text.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
text = text.replace(/(^|[^\w])___(.*?)___(?=[^\w]|$)/g, "$1<strong><em>$2</em></strong>");
|
||||
text = text.replace(/(^|[^\w])__(.*?)__(?=[^\w]|$)/g, "$1<strong>$2</strong>");
|
||||
text = text.replace(/(^|[^\w])_(.*?)_(?=[^\w]|$)/g, "$1<em>$2</em>");
|
||||
|
||||
text = text.replace(/\[\[IC(\d+)\]\]/g, (_m, idx) => inline_codes[Number(idx)] ?? _m);
|
||||
text = LinkUtils.restoreAnchors(text, anchors);
|
||||
return text.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the body is only a single emoji (after markdown strip), for large bubble rendering.
|
||||
*/
|
||||
|
|
@ -131,7 +173,8 @@ export default class MarkdownRenderer {
|
|||
// Strip headers
|
||||
text = text.replace(/^#+ (.*)$/gm, "$1");
|
||||
|
||||
// Strip bold and italic
|
||||
// Strip strikethrough, bold and italic
|
||||
text = text.replace(/~~(.*?)~~/g, "$1");
|
||||
text = text.replace(/\*\*\*(.*?)\*\*\*/g, "$1");
|
||||
text = text.replace(/\*\*(.*?)\*\*/g, "$1");
|
||||
text = text.replace(/\*(.*?)\*/g, "$1");
|
||||
|
|
|
|||
|
|
@ -527,3 +527,44 @@ describe("MarkdownRenderer.js", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderBasic (RRC / limited chat markdown)", () => {
|
||||
it("renders bold, italic, strikethrough, and inline code", () => {
|
||||
const result = MarkdownRenderer.renderBasic("**bold** *italic* ~~gone~~ `code`");
|
||||
expect(result).toContain("<strong>bold</strong>");
|
||||
expect(result).toContain("<em>italic</em>");
|
||||
expect(result).toContain("<del>gone</del>");
|
||||
expect(result).toContain("<code");
|
||||
expect(result).toContain("code");
|
||||
});
|
||||
|
||||
it("does not render headers or blockquotes", () => {
|
||||
const result = MarkdownRenderer.renderBasic("# Title\n> quote");
|
||||
expect(result).not.toContain("<h1");
|
||||
expect(result).not.toContain("blockquote");
|
||||
expect(result).toContain("# Title");
|
||||
});
|
||||
|
||||
it("keeps Nomad path field data after a single backtick as one link", () => {
|
||||
const text =
|
||||
"9ce92808be498e9e05590ff27cbfdfe4:/page/forum/thread.mu`cat=general|thread=critical-security-update-rns-139-fixes-severe-rnsh-security-flaw";
|
||||
const result = MarkdownRenderer.renderBasic(text);
|
||||
expect(result).toContain("nomadnet-link");
|
||||
expect(result).toContain(
|
||||
'data-nomadnet-url="9ce92808be498e9e05590ff27cbfdfe4:/page/forum/thread.mu`cat=general|thread=critical-security-update-rns-139-fixes-severe-rnsh-security-flaw"'
|
||||
);
|
||||
expect(result).not.toContain("<code");
|
||||
});
|
||||
|
||||
it("renders https links and escapes HTML", () => {
|
||||
const result = MarkdownRenderer.renderBasic('hi <script>alert(1)</script> https://example.com/a');
|
||||
expect(result).not.toContain("<script>");
|
||||
expect(result).toContain("<script>");
|
||||
expect(result).toContain('href="https://example.com/a"');
|
||||
});
|
||||
|
||||
it("preserves line breaks as br tags", () => {
|
||||
const result = MarkdownRenderer.renderBasic("line1\nline2");
|
||||
expect(result).toBe("line1<br>line2");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue