Compare commits

...
5 changed files with 76 additions and 41 deletions

No files matched your search

@@ -189,8 +189,9 @@ export const LexicalChannelTextareaContent = observer(
}) => {
const {i18n} = useLingui();
const composerKey = draftChannelId ?? channel.id;
const editingMessageId = MessageEdit.getEditingMessageId(channel.id);
const editingMobileMessageId = MessageEditMobile.getEditingMobileMessageId(channel.id);
const ownsChannelEdits = draftChannelId == null;
const editingMessageId = ownsChannelEdits ? MessageEdit.getEditingMessageId(channel.id) : null;
const editingMobileMessageId = ownsChannelEdits ? MessageEditMobile.getEditingMobileMessageId(channel.id) : null;
const isEditingMessageInComposer = editingMobileMessageId != null;
let initialDraftDisplay = '';
if (!disabled && !isEditingMessageInComposer && draft !== null) {
@@ -810,8 +811,7 @@ export const LexicalChannelTextareaContent = observer(
}
}, []);
const dismissTopmostComposerAffordance = useCallback((): boolean => {
const editingInline = MessageEdit.getEditingMessageId(channel.id) !== null;
if (editingInline) {
if (editingMessageId !== null) {
MessageCommands.stopEdit(channel.id);
return true;
}
@@ -846,6 +846,7 @@ export const LexicalChannelTextareaContent = observer(
channel.id,
composerKey,
editingMessageForComposer,
editingMessageId,
focusComposer,
handleCancelEdit,
handleClearSlashCommand,
@@ -4,11 +4,14 @@ import {Message as MessageComponent} from '@app/features/channel/components/Chan
import {LexicalChannelTextareaContent} from '@app/features/channel/components/LexicalChannelTextareaContent';
import type {Channel} from '@app/features/channel/models/Channel';
import * as DraftCommands from '@app/features/messaging/commands/DraftCommands';
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
import type {SendMessageFunction} from '@app/features/messaging/hooks/useMessageSubmission';
import Drafts from '@app/features/messaging/state/MessagingDrafts';
import Messages from '@app/features/messaging/state/MessagingMessages';
import {CloudUpload} from '@app/features/messaging/upload/CloudUpload';
import * as MessageSubmitUtils from '@app/features/messaging/utils/MessageSubmitUtils';
import {getParserFlagsForContext} from '@app/features/messaging/utils/markdown/MarkdownParserFlags';
import {parseAndRenderToPlaintext} from '@app/features/messaging/utils/markdown/Plaintext';
import {formatUploadingAttachmentSummary} from '@app/features/messaging/utils/UploadingAttachmentLabelUtils';
import {shouldDisableAutofocusOnMobile} from '@app/features/platform/utils/AutofocusUtils';
import * as ThreadCommands from '@app/features/threads/commands/ThreadCommands';
@@ -35,6 +38,8 @@ import {ChatsIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import {useCallback, useEffect, useRef, useState} from 'react';
const THREAD_NAME_PARSER_FLAGS = getParserFlagsForContext(MarkdownContext.STANDARD_WITHOUT_JUMBO);
interface ThreadCreatePaneProps {
parent: Channel;
messageId: string | null;
@@ -64,7 +69,19 @@ export const ThreadCreatePane = observer(({parent, messageId}: ThreadCreatePaneP
observer.observe(inertRoot, {attributes: true, attributeFilter: ['inert']});
return () => observer.disconnect();
}, []);
void Messages.version;
const sourceMessage = messageId ? Messages.getMessage(parent.id, messageId) : undefined;
const suggestedName = sourceMessage?.content
? parseAndRenderToPlaintext(sourceMessage.content, THREAD_NAME_PARSER_FLAGS, {
channelId: parent.id,
includeEmojiNames: true,
mentionChannels: sourceMessage.mentionChannels,
i18n,
})
.replace(/\s+/g, ' ')
.trim()
.slice(0, THREAD_NAME_MAX_LENGTH)
: '';
const draftKey = `thread-create:${parent.id}`;
const accountKey = Users.viewAccountKey;
const draft = Drafts.getDraft(draftKey);
@@ -78,10 +95,7 @@ export const ThreadCreatePane = observer(({parent, messageId}: ThreadCreatePaneP
const disabled = cooldownSeconds > 0 || (!canPublic && !canPrivate);
const handleSubmit: SendMessageFunction = useCallback(
(content, hasAttachments, stickersOrTts, favoriteMemeIdOrStickers, maybeFavoriteMemeId) => {
const threadName = (nameRef.current.trim() || sourceMessage?.content.trim() || '').slice(
0,
THREAD_NAME_MAX_LENGTH,
);
const threadName = (nameRef.current.trim() || suggestedName).slice(0, THREAD_NAME_MAX_LENGTH);
if (!threadName) {
setNameError(i18n._(D.THREAD_NAME_REQUIRED_DESCRIPTOR));
return false;
@@ -122,7 +136,7 @@ export const ThreadCreatePane = observer(({parent, messageId}: ThreadCreatePaneP
});
return true;
},
[draftKey, i18n, messageId, parent, privateSelected, sourceMessage],
[draftKey, i18n, messageId, parent, privateSelected, suggestedName],
);
return (
<div className={styles.pane} data-flx="threads.thread-create-pane.pane">
@@ -138,7 +152,7 @@ export const ThreadCreatePane = observer(({parent, messageId}: ThreadCreatePaneP
setNameError(undefined);
}}
label={i18n._(D.THREAD_NAME_DESCRIPTOR)}
placeholder={sourceMessage?.content.slice(0, THREAD_NAME_MAX_LENGTH) || i18n._(D.NEW_THREAD_DESCRIPTOR)}
placeholder={suggestedName || i18n._(D.NEW_THREAD_DESCRIPTOR)}
maxLength={THREAD_NAME_MAX_LENGTH}
autoComplete="off"
error={nameError}
@@ -144,33 +144,6 @@ function appendUniquePattern(patterns, pattern) {
patterns.push(normalized);
}
function inventoryPatternsForTargetPids(pids) {
if (!(binding && typeof binding.AudioBridge === 'function')) return [];
const wanted = new Set(pids.map((pid) => String(pid)));
const patterns = [];
let bridge = null;
try {
bridge = new binding.AudioBridge();
const inventory = bridge.inventory();
if (!Array.isArray(inventory)) return patterns;
for (const props of inventory) {
if (!props || typeof props !== 'object') continue;
if (props['media.class'] !== 'Stream/Output/Audio') continue;
const processId = props['application.process.id'] || props['pipewire.sec.pid'];
if (!wanted.has(String(processId || ''))) continue;
appendUniquePattern(patterns, {'object.serial': String(props['object.serial'] || '')});
appendUniquePattern(patterns, {'node.name': String(props['node.name'] || '')});
appendUniquePattern(patterns, {'client.id': String(props['client.id'] || '')});
}
} catch {
} finally {
try {
bridge?.release?.();
} catch {}
}
return patterns;
}
function routingRuleFromTarget(target, options) {
if (target && typeof target === 'object') {
return normalizeRoutingRule(target.linuxRule || target);
@@ -179,9 +152,8 @@ function routingRuleFromTarget(target, options) {
if (!Number.isSafeInteger(pid) || pid <= 0) {
throw new TypeError('ProcessLoopback target pid must be a positive integer');
}
const targetPids = targetPidList(pid, Boolean(options?.includeProcessTree));
const include = inventoryPatternsForTargetPids(targetPids);
for (const candidate of targetPids) {
const include = [];
for (const candidate of targetPidList(pid, Boolean(options?.includeProcessTree))) {
appendUniquePattern(include, {'application.process.id': String(candidate)});
appendUniquePattern(include, {'pipewire.sec.pid': String(candidate)});
}
@@ -98,6 +98,17 @@ describe('renderTrayIcon', () => {
assert.deepEqual(pixel(bitmap, 32, 6, 16), [0xff, 0xff, 0xff, 0xff]);
});
test('keeps the Linux monochrome pixmap within 256 pixels', () => {
const result = renderTrayIcon(FakeNativeImage.fromBitmap(256), 'white', false, 'linux');
const [representation] = result.representations.values();
assert.equal(representation.width, 256);
assert.equal(representation.height, 256);
const bitmap = result.toBitmap();
assert.deepEqual(pixel(bitmap, 256, 128, 50), [0xff, 0xff, 0xff, 0xff]);
assert.deepEqual(pixel(bitmap, 256, 128, 128), [0, 0, 0, 0]);
assert.deepEqual(pixel(bitmap, 256, 20, 128), [0, 0, 0, 0]);
});
test('skips representations whose bitmap does not match their size', () => {
const base = FakeNativeImage.fromBitmap(16);
base.toBitmap = () => Buffer.alloc(3);
+38 -1
View File
@@ -18,6 +18,7 @@ const MIN_GLYPH_CONTRAST = 32;
const GLYPH_CUTOUT_START = 0.1;
const GLYPH_CUTOUT_END = 0.5;
const LINUX_MONOCHROME_CANVAS_RATIO = 22 / 16;
const LINUX_MAX_PIXMAP_SIZE = 256;
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const DOT_BGR: Bgr = [0x00, 0x00, 0xdd];
const TEMPLATE_DOT_BGR: Bgr = [0x00, 0x00, 0x00];
@@ -149,6 +150,36 @@ function cropTrayGlyph(buffer: Buffer, width: number, height: number): TrayBitma
return {buffer: target, width: size, height: size};
}
function downscaleTrayBitmap(bitmap: TrayBitmap, size: number): TrayBitmap {
if (bitmap.width <= size) return bitmap;
const scale = bitmap.width / size;
const buffer = Buffer.alloc(size * size * 4);
const sums = [0, 0, 0, 0];
for (let y = 0; y < size; y++) {
const top = y * scale;
const bottom = top + scale;
for (let x = 0; x < size; x++) {
const left = x * scale;
const right = left + scale;
sums.fill(0);
for (let sy = Math.floor(top); sy < Math.min(bitmap.height, Math.ceil(bottom)); sy++) {
const weightY = Math.min(bottom, sy + 1) - Math.max(top, sy);
for (let sx = Math.floor(left); sx < Math.min(bitmap.width, Math.ceil(right)); sx++) {
const weight = weightY * (Math.min(right, sx + 1) - Math.max(left, sx));
const offset = (sy * bitmap.width + sx) * 4;
for (let channel = 0; channel < 4; channel++) {
sums[channel] += bitmap.buffer[offset + channel] * weight;
}
}
}
for (let channel = 0; channel < 4; channel++) {
buffer[(y * size + x) * 4 + channel] = Math.round(sums[channel] / (scale * scale));
}
}
}
return {buffer, width: size, height: size};
}
function addMonochromeTrayMargins(bitmap: TrayBitmap, size: number): TrayBitmap {
const inset = Math.floor((size - bitmap.width) / 2);
const buffer = Buffer.alloc(size * size * 4);
@@ -210,7 +241,13 @@ export function renderTrayIcon(
appearance === 'template' ? TEMPLATE_DOT_BGR : DOT_BGR,
);
}
if (linuxMonochrome) bitmap = addMonochromeTrayMargins(bitmap, Math.ceil(logicalCanvasSize * scaleFactor));
if (linuxMonochrome) {
const canvasSize = Math.min(Math.ceil(logicalCanvasSize * scaleFactor), LINUX_MAX_PIXMAP_SIZE);
bitmap = addMonochromeTrayMargins(
downscaleTrayBitmap(bitmap, Math.floor(canvasSize / LINUX_MONOCHROME_CANVAS_RATIO)),
canvasSize,
);
}
result.addRepresentation({scaleFactor, ...bitmap});
}
if (result.isEmpty()) {