mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 14:05:48 +09:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8d43af191 | ||
|
|
1a772daa47 | ||
|
|
a6bde3aae4 | ||
|
|
c51800a8d6 |
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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in new issue
Block a user