Compare commits

...
Author SHA1 Message Date
Hampus da3922586a fix(desktop): restore Windows canary builds (#2017) 2026-08-25 23:54:33 +02:00
Hampus 28184f8d4d chore(desktop): downgrade Electron to 42.10.0 (#2016) 2026-08-25 23:21:20 +02:00
fluxer-ci[bot]andhampus-fluxer a4e7522ca0 chore(i18n): update public marketing catalogs (#2015)
Co-authored-by: hampus-fluxer <[email protected]>
2026-08-25 21:42:18 +02:00
fluxer-ci[bot]andhampus-fluxer 59b3d30323 chore(marketing): advance pointer 1915e59 → 530c44e (#2014)
Co-authored-by: hampus-fluxer <[email protected]>
2026-08-25 21:41:56 +02:00
Hampus 53cac0b614 style(markdown): format the escaped emoji branch (#2012) 2026-08-25 14:48:30 +02:00
Hampus 82c29398d3 fix(markdown): render an escaped raw emoji as a plain glyph (#2011) 2026-08-25 14:24:20 +02:00
Hampus 6d289e31c7 fix(app): make a backslash before an emoji node keep it literal (#2010) 2026-08-25 14:23:52 +02:00
Hampus 5ec02c3089 fix(tools): stop the wasm clang test depending on an executable tempfile (#2009) 2026-08-25 14:23:18 +02:00
Hampus 9940273cd9 docs: drop the release status notices from the readme (#2008) 2026-08-25 14:09:54 +02:00
Hampus 21c7b9872e fix(app): show composer autocomplete above modal surfaces (#2007) 2026-08-25 13:48:59 +02:00
Hampus fa99ec6f8f fix(app): keep a backslash-escaped emoji literal in the composer (#2006) 2026-08-25 13:45:53 +02:00
Hampus 78f7db9250 fix(app): convert pasted and edited unicode emoji in the composer (#2005) 2026-08-25 13:44:26 +02:00
Hampus c101610c46 fix(markdown): render an escaped emoji shortcode as its raw glyph (#2004) 2026-08-25 13:42:51 +02:00
44 changed files with 293 additions and 158 deletions

No files matched your search

-12
View File
@@ -1,15 +1,3 @@
> [!CAUTION]
> As of this writing (15 June 2026), we are working to finalise the API and self-hosting documentation over the next few days.
>
> We apologise for the brief delay in open-source releases. We paused after spam waves created safety concerns while we built out Fluxer's trust and safety infrastructure. During that same stretch, we have been fixing hundreds of bugs, adding new features, and preparing a much improved audio and video system.
>
> You can already try that work in the Fluxer Canary client: [download Canary](https://canary.fluxer.app/download) or [open Canary on the web](https://web.canary.fluxer.app). The latest stable client remains out of date for now, but over the coming weeks we are finalising the remaining work needed to stabilise the current latest code out in the open.
> [!NOTE]
> Learn about the developer behind Fluxer, the goals of the project, the tech stack, and what's coming next.
>
> [Read the launch blog post](https://blog.fluxer.app/how-i-built-fluxer-a-discord-like-chat-app/) | [View full roadmap](https://blog.fluxer.app/roadmap-2026/)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./fluxer_static/marketing/branding/logo-white.svg">
@@ -3,7 +3,7 @@
.container {
overflow: hidden;
background-color: var(--background-primary);
z-index: 2;
z-index: var(--z-index-popout);
}
.containerAttached {
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import UnicodeEmojis from '@app/features/expressions/utils/UnicodeEmojis';
import {
$captureSelectionOffsets,
$getComposerNodeDisplayStart,
@@ -12,7 +13,7 @@ import {
$isComposerStandardEmojiNode,
} from '@app/features/lexical/composer/nodes/ComposerStandardEmojiNode';
import {$isSyntaxMarkerNode} from '@app/features/lexical/composer/nodes/SyntaxMarkerNode';
import {findTypedEmojiShortcode} from '@app/features/messaging/utils/markdown/TypedEmojiMatch';
import {findTypedEmojiShortcode, type TypedEmojiMatch} from '@app/features/messaging/utils/markdown/TypedEmojiMatch';
import type {ResolvedTypedEmoji} from '@app/features/messaging/utils/TypedEmojiShortcodeUtils';
import {type LexicalEditor, TextNode} from 'lexical';
@@ -26,6 +27,46 @@ export function registerComposerEmojiShortcode(editor: LexicalEditor, resolve: C
});
}
function findUnicodeEmoji(text: string, startIndex: number): TypedEmojiMatch | null {
const pattern = new RegExp(UnicodeEmojis.EMOJI_SURROGATE_RE.source, 'g');
pattern.lastIndex = startIndex;
const match = pattern.exec(text);
if (match == null) {
return null;
}
const name = UnicodeEmojis.nameForSurrogate(match[0], false);
if (!name) {
return null;
}
return {start: match.index, end: match.index + match[0].length, name};
}
interface EmojiToken extends TypedEmojiMatch {
surrogate: boolean;
}
function findNextEmojiToken(text: string, startIndex: number): EmojiToken | null {
const shortcode = findTypedEmojiShortcode(text, startIndex);
const surrogate = findUnicodeEmoji(text, startIndex);
if (shortcode == null) {
return surrogate == null ? null : {...surrogate, surrogate: true};
}
if (surrogate == null) {
return {...shortcode, surrogate: false};
}
return surrogate.start < shortcode.start ? {...surrogate, surrogate: true} : {...shortcode, surrogate: false};
}
function isEscapedAt(text: string, index: number): boolean {
let backslashes = 0;
let cursor = index - 1;
while (cursor >= 0 && text[cursor] === '\\') {
backslashes += 1;
cursor -= 1;
}
return backslashes % 2 === 1;
}
export function $convertEmojiShortcode(node: TextNode, resolve: ComposerEmojiResolver): void {
if ($isSyntaxMarkerNode(node) || node.hasFormat('code')) {
return;
@@ -37,7 +78,7 @@ export function $convertEmojiShortcode(node: TextNode, resolve: ComposerEmojiRes
const text = node.getTextContent();
let searchFrom = 0;
while (true) {
const match = findTypedEmojiShortcode(text, searchFrom);
const match = findNextEmojiToken(text, searchFrom);
if (match == null) {
return;
}
@@ -45,6 +86,22 @@ export function $convertEmojiShortcode(node: TextNode, resolve: ComposerEmojiRes
if ((codeMarkers == null ? 0 : codeMarkers.length) % 2 === 1) {
return;
}
if (isEscapedAt(text, match.start)) {
if (match.surrogate) {
const shortcodeText = `:${match.name}:`;
const selection = $captureSelectionOffsets();
const nodeDisplayStart = $getComposerNodeDisplayStart(node);
const replaced = `${text.slice(0, match.start)}${shortcodeText}${text.slice(match.end)}`;
node.setTextContent(replaced);
if (selection != null) {
const adjusted = $adjustEmbedCaret(selection, nodeDisplayStart, match.start, match.end, shortcodeText.length);
$selectComposerRange(adjusted.anchor, adjusted.focus);
}
return;
}
searchFrom = match.end;
continue;
}
const previous = match.start === 0 ? node.getPreviousSibling() : null;
if (/^skin-tone-[1-5]$/.test(match.name) && $isComposerStandardEmojiNode(previous)) {
const baseName = previous.getEmojiName().split('::')[0]!;
@@ -139,6 +139,16 @@ function $reconcileParagraph(paragraph: ParagraphNode, parserFlags?: number): vo
}
}
function $isEscapedAtOffset(source: string, offset: number): boolean {
let backslashes = 0;
let cursor = offset - 1;
while (cursor >= 0 && source[cursor] === '\\') {
backslashes += 1;
cursor -= 1;
}
return backslashes % 2 === 1;
}
function $reconcileLine(
line: Array<LexicalNode>,
parserFlags?: number,
@@ -153,13 +163,15 @@ function $reconcileLine(
const nodeText = $nodeWireText(node);
const nodeEnd = sourceOffset + nodeText.length;
if ($isComposerMentionNode(node) || $isComposerCustomEmojiNode(node) || $isComposerStandardEmojiNode(node)) {
const literal = spans.some(
(span) =>
span.role === 'content' &&
(span.format & MarkdownHl.code) !== 0 &&
span.start <= sourceOffset &&
span.end >= nodeEnd,
);
const literal =
$isEscapedAtOffset(source, sourceOffset) ||
spans.some(
(span) =>
span.role === 'content' &&
(span.format & MarkdownHl.code) !== 0 &&
span.start <= sourceOffset &&
span.end >= nodeEnd,
);
if (node.isLiteral() !== literal) {
node.setLiteral(literal);
}
+1 -1
View File
@@ -25,7 +25,7 @@
"devDependencies": {
"@types/node": "24.10.9",
"@typescript/native-preview": "7.0.0-dev.20260130.1",
"electron": "43.4.0",
"electron": "42.10.0",
"electron-builder": "26.9.0",
"esbuild": "0.27.2",
"typescript": "5.9.3"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: bg\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: el\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: en-GB\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: en-US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: es-419\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: es-ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: fi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: he\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: hi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: ko\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: lt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: no\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: pt-BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: ro\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: sv-SE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: th\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: vi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: zh-CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+2 -2
View File
@@ -4,8 +4,8 @@
msgid ""
msgstr ""
"Project-Id-Version: fluxer-marketing\n"
"POT-Creation-Date: 2026-08-24 00:00+0000\n"
"PO-Revision-Date: 2026-08-24 00:00+0000\n"
"POT-Creation-Date: 2026-08-25 00:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Language: zh-TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -106,6 +106,25 @@ fn parse_inline_with_context(
position += 2;
continue;
}
if next == b':'
&& let Some(result) = links::parse_emoji_shortcode(parser, &text[position + 1..])
&& let Node::Emoji {
kind: EmojiKind::Standard { raw, .. },
} = &result.node
{
accumulated.extend_from_slice(raw.as_bytes());
position += 1 + result.advance;
continue;
}
if let Some(emoji) = parser
.emoji_context()
.standard_at(base_offset + position + 1)
&& text[position + 1..].starts_with(&emoji.raw)
{
accumulated.extend_from_slice(emoji.raw.as_bytes());
position += 1 + emoji.len;
continue;
}
if is_escapable_character(next)
|| is_ordered_list_marker_dot_escape(text, position)
|| is_word_dot_escape(text, position)
+5 -5
View File
@@ -1680,8 +1680,8 @@ importers:
specifier: 7.0.0-dev.20260130.1
version: 7.0.0-dev.20260130.1
electron:
specifier: 43.4.0
version: 43.4.0
specifier: 42.10.0
version: 42.10.0
electron-builder:
specifier: 26.9.0
version: 26.9.0([email protected])
@@ -8222,8 +8222,8 @@ packages:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'}
electron@43.4.0:
resolution: {integrity: sha512-3qxGF0CeQbiox5oWV1JlbWGQ1VerbmDhTFqW4sJ8h7uqTHniFYPObXJcDna0DMh32et0fFyKzz0YY8lJv3t5jg==}
electron@42.10.0:
resolution: {integrity: sha512-yBWnZO8tKId0ADvGa4+imPmm3+8Kfs8MtkUPhQQJz1AjTnUUDwCTWkQEU0CrArVVL97Jo4Q/0mVcLHciicBDGQ==}
engines: {node: '>= 22.12.0'}
hasBin: true
@@ -18314,7 +18314,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron@43.4.0:
electron@42.10.0:
dependencies:
'@electron-internal/extract-zip': 1.0.5
'@electron/get': 5.1.0
+14 -22
View File
@@ -78,13 +78,20 @@ fn wasm_clang_candidates() -> Vec<PathBuf> {
}
fn discover_wasm_clang_from(candidates: Vec<PathBuf>) -> Result<PathBuf> {
discover_wasm_clang_with(candidates, clang_targets_wasm32)
}
fn discover_wasm_clang_with(
candidates: Vec<PathBuf>,
targets_wasm32: impl Fn(&Path) -> bool,
) -> Result<PathBuf> {
let mut seen: Vec<PathBuf> = Vec::new();
for candidate in candidates {
if seen.contains(&candidate) {
continue;
}
seen.push(candidate.clone());
if clang_targets_wasm32(&candidate) {
if targets_wasm32(&candidate) {
return Ok(candidate);
}
}
@@ -495,19 +502,13 @@ export const MARKDOWN_PARSER_WASM_BASE64 =\n\
#[test]
fn wasm_clang_discovery_prefers_a_wasm32_capable_candidate_over_an_earlier_one() {
let temp = TempDir::new().expect("temp dir");
let clang = temp.path().join("clang");
fs::write(
&clang,
"#!/bin/sh\nif [ \"$1\" = \"--print-targets\" ]; then echo ' wasm32 - WebAssembly 32-bit'; fi\n",
let capable = PathBuf::from("/opt/llvm/bin/clang");
let found = discover_wasm_clang_with(
vec![PathBuf::from("/nonexistent/clang"), capable.clone()],
|candidate| candidate == capable,
)
.expect("write fake clang");
make_executable(&clang);
let found =
discover_wasm_clang_from(vec![PathBuf::from("/nonexistent/clang"), clang.clone()])
.expect("the fake clang advertises wasm32");
assert_eq!(found, clang);
.expect("the capable candidate advertises wasm32");
assert_eq!(found, capable);
}
#[test]
@@ -521,15 +522,6 @@ export const MARKDOWN_PARSER_WASM_BASE64 =\n\
assert_eq!(discover_wasm_ar(&clang), Some(archiver));
}
#[cfg(unix)]
fn make_executable(path: &Path) {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).expect("chmod");
}
#[cfg(not(unix))]
fn make_executable(_path: &Path) {}
#[test]
fn libfluxcore_bindgen_dts_reset_hook_is_inserted() {
let content = "export function is_animated_image(input: Uint8Array): boolean;\nexport type InitInput = RequestInfo;";
+106 -39
View File
@@ -897,10 +897,32 @@ fn apt_get(args: &[&str]) -> Result<()> {
}
fn install_msvc_arm64_tools_step() -> Result<()> {
let installer =
Path::new(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe");
let install_path = Path::new(r"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools");
run_command(CommandSpec::new(installer).args([
let program_files_x86 = env::var_os("ProgramFiles(x86)")
.map(PathBuf::from)
.ok_or_else(|| anyhow!("ProgramFiles(x86) is not set on the Windows runner"))?;
let installer_dir = program_files_x86
.join("Microsoft Visual Studio")
.join("Installer");
let installer = installer_dir.join("setup.exe");
let vswhere = installer_dir.join("vswhere.exe");
ensure!(
vswhere.is_file(),
"Visual Studio locator not found: {}",
vswhere.display()
);
let install_path = resolve_visual_studio_install_path(&vswhere)?;
if let Some(linker) = find_msvc_arm64_linker(&install_path)? {
println!("ARM64 cross link.exe: {}", linker.display());
return Ok(());
}
ensure!(
installer.is_file(),
"Visual Studio installer not found: {}",
installer.display()
);
run_command(CommandSpec::new(&installer).args([
"modify",
"--installPath",
install_path.to_string_lossy().as_ref(),
@@ -924,30 +946,66 @@ fn install_msvc_arm64_tools_step() -> Result<()> {
"VS installer did not finish within the timeout."
);
let mut found = false;
let linker = find_msvc_arm64_linker(&install_path)?.ok_or_else(|| {
anyhow!(
"ARM64 cross-build tools were not installed under {}\\VC\\Tools\\MSVC\\*\\bin\\HostX64\\arm64",
install_path.display()
)
})?;
println!("ARM64 cross link.exe: {}", linker.display());
Ok(())
}
fn resolve_visual_studio_install_path(vswhere: &Path) -> Result<PathBuf> {
let output = output_text(CommandSpec::new(vswhere).args([
"-products",
"*",
"-latest",
"-prerelease",
"-property",
"installationPath",
]))?;
let paths = output
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>();
ensure!(
paths.len() == 1,
"vswhere returned {} Visual Studio installation paths; expected exactly one",
paths.len()
);
let install_path = PathBuf::from(paths[0]);
ensure!(
install_path.is_dir(),
"vswhere returned a missing Visual Studio installation: {}",
install_path.display()
);
println!("Visual Studio installation: {}", install_path.display());
Ok(install_path)
}
fn find_msvc_arm64_linker(install_path: &Path) -> Result<Option<PathBuf>> {
let msvc_root = install_path.join("VC").join("Tools").join("MSVC");
if msvc_root.exists() {
for entry in fs::read_dir(&msvc_root)
.with_context(|| format!("Failed to read {}", msvc_root.display()))?
{
let candidate = entry?
.path()
.join("bin")
.join("HostX64")
.join("arm64")
.join("link.exe");
if candidate.exists() {
println!("ARM64 cross link.exe: {}", candidate.display());
found = true;
}
if !msvc_root.is_dir() {
return Ok(None);
}
let mut linkers = Vec::new();
for entry in fs::read_dir(&msvc_root)
.with_context(|| format!("Failed to read {}", msvc_root.display()))?
{
let candidate = entry?
.path()
.join("bin")
.join("HostX64")
.join("arm64")
.join("link.exe");
if candidate.is_file() {
linkers.push(candidate);
}
}
ensure!(
found,
"ARM64 cross-build tools were not installed under {}\\*\\bin\\HostX64\\arm64.",
msvc_root.display()
);
Ok(())
linkers.sort();
Ok(linkers.pop())
}
fn windows_installer_process_running() -> Result<bool> {
@@ -1823,7 +1881,16 @@ fn create_portable_zip_windows_step() -> Result<()> {
}
const FLUXER_WINDOWS_SIGNER_COMMON_NAME: &str = "Fluxer Platform AB";
const THIRD_PARTY_PUBLISHER_ALLOWLIST: &[&str] = &[];
const THIRD_PARTY_WINDOWS_SIGNATURE_ALLOWLIST: &[(&str, &str)] = &[
(
"d3dcompiler_47.dll",
"CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US",
),
(
"dxil.dll",
"CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US",
),
];
const KNOWN_OPTIONAL_WINDOWS_PE_INVENTORY: &[&str] = &["fluxer-vulkan-layer.win32-ia32-msvc.dll"];
const WINDOWS_NATIVE_ADDON_STEMS: &[&str] = &[
"hardware-encoder",
@@ -2144,7 +2211,7 @@ fn assert_fluxer_signed(row: &SignatureRow) -> Result<()> {
Ok(())
}
fn assert_third_party_signed(row: &SignatureRow) -> Result<()> {
fn assert_third_party_signed(row: &SignatureRow, relative: &str) -> Result<()> {
ensure!(
row.status == "Valid",
"Authenticode status is {} (expected Valid)",
@@ -2154,11 +2221,13 @@ fn assert_third_party_signed(row: &SignatureRow) -> Result<()> {
.subject
.as_deref()
.ok_or_else(|| anyhow!("Authenticode signature has no signer certificate subject"))?;
let common_name = certificate_common_name(subject)
.ok_or_else(|| anyhow!("Signer subject has no CN= component: {subject}"))?;
ensure!(
THIRD_PARTY_PUBLISHER_ALLOWLIST.contains(&common_name),
"Signer CN '{common_name}' is not an allowlisted third-party publisher"
THIRD_PARTY_WINDOWS_SIGNATURE_ALLOWLIST
.iter()
.any(|(allowed_path, allowed_subject)| {
relative.eq_ignore_ascii_case(allowed_path) && subject == *allowed_subject
}),
"Signer subject '{subject}' is not allowlisted for {relative}"
);
ensure!(
row.ts_subject.is_some(),
@@ -2167,10 +2236,10 @@ fn assert_third_party_signed(row: &SignatureRow) -> Result<()> {
Ok(())
}
fn assert_signed_by_known_publisher(row: &SignatureRow) -> Result<()> {
fn assert_signed_by_known_publisher(row: &SignatureRow, relative: &str) -> Result<()> {
match assert_fluxer_signed(row) {
Ok(()) => Ok(()),
Err(fluxer_error) => assert_third_party_signed(row)
Err(fluxer_error) => assert_third_party_signed(row, relative)
.map_err(|third_party_error| anyhow!("{fluxer_error}; {third_party_error}")),
}
}
@@ -2333,22 +2402,20 @@ fn verify_windows_pe_signatures(
));
continue;
};
if let Err(error) = assert_signed_by_known_publisher(row) {
if let Err(error) = assert_signed_by_known_publisher(row, &relative) {
failures.push(format!("{relative}: {error}"));
}
}
ensure!(
failures.is_empty(),
"{label}: {} of {} Windows binaries are not signed by '{}':\n{}",
"{label}: {} of {} Windows binaries do not have an approved signature:\n{}",
failures.len(),
files.len(),
FLUXER_WINDOWS_SIGNER_COMMON_NAME,
failures.join("\n")
);
println!(
"{label}: verified {} Windows binaries signed by '{}'.",
files.len(),
FLUXER_WINDOWS_SIGNER_COMMON_NAME
"{label}: verified {} Windows binary signatures.",
files.len()
);
Ok(())
}