mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
feat(app): toggle for sequential msg file send order (#1245)
Co-authored-by: Hampus <hampus@fluxer.app>
This commit is contained in:
@@ -601,6 +601,7 @@ export interface AccessibilitySettings {
|
||||
stayInteractiveWhenUnfocused: boolean;
|
||||
firstClickPassThroughWhenUnfocused: boolean;
|
||||
scrollToBottomOnMessageSend: boolean;
|
||||
sequentialFileSend: boolean;
|
||||
showNeko: boolean;
|
||||
keepNekoStill: boolean;
|
||||
showVideoSeekPreviewThumbnails: boolean;
|
||||
@@ -718,6 +719,7 @@ class Accessibility {
|
||||
stayInteractiveWhenUnfocused = false;
|
||||
firstClickPassThroughWhenUnfocused = false;
|
||||
scrollToBottomOnMessageSend = true;
|
||||
sequentialFileSend = false;
|
||||
showNeko = false;
|
||||
keepNekoStill = false;
|
||||
showVideoSeekPreviewThumbnails = false;
|
||||
@@ -828,6 +830,7 @@ class Accessibility {
|
||||
'stayInteractiveWhenUnfocused',
|
||||
'firstClickPassThroughWhenUnfocused',
|
||||
'scrollToBottomOnMessageSend',
|
||||
'sequentialFileSend',
|
||||
],
|
||||
toMessage: (s) => ({
|
||||
saturationFactor: s.saturationFactor,
|
||||
@@ -888,6 +891,7 @@ class Accessibility {
|
||||
stayInteractiveWhenUnfocused: s.stayInteractiveWhenUnfocused,
|
||||
firstClickPassThroughWhenUnfocused: s.firstClickPassThroughWhenUnfocused,
|
||||
scrollToBottomOnMessageSend: s.scrollToBottomOnMessageSend,
|
||||
sequentialFileSend: s.sequentialFileSend,
|
||||
}),
|
||||
applyMessage: (s, m) => {
|
||||
if (m.saturationFactor !== undefined) s.saturationFactor = m.saturationFactor;
|
||||
@@ -978,6 +982,7 @@ class Accessibility {
|
||||
if (m.firstClickPassThroughWhenUnfocused !== undefined)
|
||||
s.firstClickPassThroughWhenUnfocused = m.firstClickPassThroughWhenUnfocused;
|
||||
if (m.scrollToBottomOnMessageSend !== undefined) s.scrollToBottomOnMessageSend = m.scrollToBottomOnMessageSend;
|
||||
if (m.sequentialFileSend !== undefined) s.sequentialFileSend = m.sequentialFileSend;
|
||||
},
|
||||
});
|
||||
await this.applyStoredZoom();
|
||||
@@ -1314,6 +1319,8 @@ class Accessibility {
|
||||
this.firstClickPassThroughWhenUnfocused = validated.firstClickPassThroughWhenUnfocused;
|
||||
if (validated.scrollToBottomOnMessageSend !== undefined)
|
||||
this.scrollToBottomOnMessageSend = validated.scrollToBottomOnMessageSend;
|
||||
if (validated.sequentialFileSend !== undefined)
|
||||
this.sequentialFileSend = validated.sequentialFileSend;
|
||||
if (validated.showNeko !== undefined && validated.showNeko !== this.showNeko) {
|
||||
this.showNeko = validated.showNeko;
|
||||
persistLocalShowNeko(validated.showNeko);
|
||||
@@ -1414,6 +1421,7 @@ class Accessibility {
|
||||
firstClickPassThroughWhenUnfocused:
|
||||
data.firstClickPassThroughWhenUnfocused ?? this.firstClickPassThroughWhenUnfocused,
|
||||
scrollToBottomOnMessageSend: data.scrollToBottomOnMessageSend ?? this.scrollToBottomOnMessageSend,
|
||||
sequentialFileSend: data.sequentialFileSend ?? this.sequentialFileSend,
|
||||
showNeko: data.showNeko ?? this.showNeko,
|
||||
keepNekoStill: data.keepNekoStill ?? this.keepNekoStill,
|
||||
showVideoSeekPreviewThumbnails: data.showVideoSeekPreviewThumbnails ?? this.showVideoSeekPreviewThumbnails,
|
||||
|
||||
@@ -44,6 +44,7 @@ import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
|
||||
import {failureCode} from '@app/features/platform/utils/ResponseInspection';
|
||||
import * as ReadStateCommands from '@app/features/read_state/commands/ReadStateCommands';
|
||||
import type {RestResponse} from '@app/features/platform/types/TransportTypes';
|
||||
import ReadStates from '@app/features/read_state/state/ReadStates';
|
||||
import * as SlowmodeCommands from '@app/features/slowmode/commands/SlowmodeCommands';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
@@ -426,6 +427,61 @@ export async function fetchMessages(
|
||||
return promise;
|
||||
}
|
||||
|
||||
interface SequentialSendEntry {
|
||||
task: () => Promise<RestResponse<WireMessage> | undefined>;
|
||||
resolve: (value: RestResponse<WireMessage> | undefined) => void;
|
||||
}
|
||||
|
||||
interface ChannelSendOrderState {
|
||||
nextOrder: number;
|
||||
nextExpected: number;
|
||||
pending: Map<number, SequentialSendEntry>;
|
||||
processing: boolean;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const channelSendOrders = new Map<string, ChannelSendOrderState>();
|
||||
|
||||
function getOrCreateChannelState(channelId: string): ChannelSendOrderState {
|
||||
let state = channelSendOrders.get(channelId);
|
||||
if (!state) {
|
||||
state = {nextOrder: 0, nextExpected: 0, pending: new Map(), processing: false, channelId};
|
||||
channelSendOrders.set(channelId, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function orderedSendImmediately(
|
||||
channelId: string,
|
||||
order: number,
|
||||
task: () => Promise<RestResponse<WireMessage> | undefined>,
|
||||
): Promise<RestResponse<WireMessage> | undefined> {
|
||||
return new Promise<RestResponse<WireMessage> | undefined>((resolve) => {
|
||||
const state = getOrCreateChannelState(channelId);
|
||||
state.pending.set(order, {task, resolve});
|
||||
void processSequentialQueue(state);
|
||||
});
|
||||
}
|
||||
|
||||
async function processSequentialQueue(state: ChannelSendOrderState): Promise<void> {
|
||||
if (state.processing) return;
|
||||
state.processing = true;
|
||||
try {
|
||||
while (state.pending.has(state.nextExpected)) {
|
||||
const entry = state.pending.get(state.nextExpected)!;
|
||||
state.pending.delete(state.nextExpected);
|
||||
state.nextExpected++;
|
||||
const result = await entry.task();
|
||||
entry.resolve(result);
|
||||
}
|
||||
} finally {
|
||||
state.processing = false;
|
||||
if (state.pending.size === 0 && state.nextExpected === state.nextOrder) {
|
||||
channelSendOrders.delete(state.channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareSendAttachments(
|
||||
channelId: string,
|
||||
params: SendMessageParams,
|
||||
@@ -445,13 +501,24 @@ async function prepareSendAttachments(
|
||||
return {attachments: prepared.attachments, files: prepared.files};
|
||||
}
|
||||
|
||||
function nextChannelOrder(channelId: string): number {
|
||||
return getOrCreateChannelState(channelId).nextOrder++;
|
||||
}
|
||||
|
||||
export async function send(channelId: string, params: SendMessageParams): Promise<WireMessage | null> {
|
||||
if (!MessageQueue.consumeLocalSendReservation(channelId, params.nonce)) {
|
||||
MessageQueue.rejectLocalRateLimitedSend(channelId, params.nonce, params.hasAttachments);
|
||||
return null;
|
||||
}
|
||||
const sendOrder =
|
||||
Accessibility.sequentialFileSend && params.hasAttachments ? nextChannelOrder(channelId) : -1;
|
||||
const prepared = await prepareSendAttachments(channelId, params);
|
||||
if (!prepared) return null;
|
||||
if (!prepared) {
|
||||
if (Accessibility.sequentialFileSend) {
|
||||
orderedSendImmediately(channelId, sendOrder, () => Promise.resolve(undefined));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const payload = {
|
||||
type: 'send' as const,
|
||||
channelId,
|
||||
@@ -469,7 +536,9 @@ export async function send(channelId: string, params: SendMessageParams): Promis
|
||||
};
|
||||
if (params.hasAttachments) {
|
||||
logger.debug(`Sending attachment message immediately for channel ${channelId}`);
|
||||
const result = await MessageQueue.sendImmediately(payload);
|
||||
const result = Accessibility.sequentialFileSend
|
||||
? await orderedSendImmediately(channelId, sendOrder, () => MessageQueue.sendImmediately(payload))
|
||||
: await MessageQueue.sendImmediately(payload);
|
||||
if (result?.body) {
|
||||
logger.debug(`Attachment message sent successfully in channel ${channelId}`);
|
||||
Messages.handleIncomingMessage({channelId, message: result.body});
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
SaveGifFavoritesControl,
|
||||
ScrollToBottomOnSendControl,
|
||||
SearchEnginesControl,
|
||||
SequentialFileSendControl,
|
||||
SkipMarkAllAsReadControl,
|
||||
StripTrackingControl,
|
||||
TranslatorsControl,
|
||||
@@ -76,6 +77,7 @@ export const DIRECT_CONTROL_ITEM_IDS = new Set([
|
||||
'chat-settings-input-buttons',
|
||||
'chat-settings-convert-emoticons',
|
||||
'chat-settings-preupload-attachments',
|
||||
'chat-settings-sequential-file-send',
|
||||
'chat-settings-scroll-to-bottom-on-send',
|
||||
'chat-settings-skip-mark-all-as-read-confirmation',
|
||||
'chat-settings-hide-muted-channels',
|
||||
@@ -115,6 +117,7 @@ export const COMPACT_SWITCH_CONTROL_ITEM_IDS = new Set([
|
||||
'chat-settings-trust-domains',
|
||||
'chat-settings-convert-emoticons',
|
||||
'chat-settings-preupload-attachments',
|
||||
'chat-settings-sequential-file-send',
|
||||
'chat-settings-scroll-to-bottom-on-send',
|
||||
'chat-settings-skip-mark-all-as-read-confirmation',
|
||||
'chat-settings-hide-muted-channels',
|
||||
@@ -240,6 +243,10 @@ export const AdvancedSettingControl = observer(({item}: {item: SearchableSetting
|
||||
return (
|
||||
<PreuploadMessageAttachmentsControl data-flx="user.advanced-setting-direct-controls.advanced-setting-control.preupload-message-attachments-control" />
|
||||
);
|
||||
case 'chat-settings-sequential-file-send':
|
||||
return (
|
||||
<SequentialFileSendControl data-flx="user.advanced-setting-direct-controls.advanced-setting-control.sequential-file-send-control" />
|
||||
);
|
||||
case 'chat-settings-scroll-to-bottom-on-send':
|
||||
return (
|
||||
<ScrollToBottomOnSendControl data-flx="user.advanced-setting-direct-controls.advanced-setting-control.scroll-to-bottom-on-send-control" />
|
||||
|
||||
+17
@@ -109,6 +109,10 @@ const UPLOAD_ATTACHMENTS_BEFORE_SENDING_DESCRIPTOR = msg({
|
||||
message: 'Upload attachments before sending',
|
||||
comment: 'Short label for an advanced message input privacy preference.',
|
||||
});
|
||||
const SEND_FILES_IN_SEQUENTIAL_ORDER_DESCRIPTOR = msg({
|
||||
message: 'Send file messages in order',
|
||||
comment: 'Short label for an advanced message input preference.',
|
||||
});
|
||||
const SHOW_STICKERS_BUTTON_DESCRIPTOR = msg({
|
||||
message: 'Show stickers button',
|
||||
comment: 'Short label for an advanced message input preference.',
|
||||
@@ -579,6 +583,19 @@ export const PreuploadMessageAttachmentsControl = observer(() => {
|
||||
);
|
||||
});
|
||||
|
||||
export const SequentialFileSendControl = observer(() => {
|
||||
const {i18n} = useLingui();
|
||||
return (
|
||||
<Switch
|
||||
ariaLabel={i18n._(SEND_FILES_IN_SEQUENTIAL_ORDER_DESCRIPTOR)}
|
||||
value={Accessibility.sequentialFileSend}
|
||||
onChange={(value) => AccessibilityCommands.update({sequentialFileSend: value})}
|
||||
compact
|
||||
data-flx="user.advanced-settings-tab.switch.sequential-file-send"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const ScrollToBottomOnSendControl = observer(() => {
|
||||
const {i18n} = useLingui();
|
||||
return (
|
||||
|
||||
+40
@@ -124,6 +124,30 @@ const FILE_UPLOAD_DESCRIPTOR = msg({
|
||||
message: 'File upload',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const SEQUENTIAL_FILE_SEND_DESCRIPTOR = msg({
|
||||
message: 'Send file messages in order',
|
||||
comment: 'Settings search entry label. Names the settings search entry in the settings UI.',
|
||||
});
|
||||
const SEQUENTIAL_SEND_DESCRIPTOR = msg({
|
||||
message: 'Sequential send',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const FILE_ORDERING_DESCRIPTOR = msg({
|
||||
message: 'File ordering',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const ORDERED_UPLOAD_DESCRIPTOR = msg({
|
||||
message: 'Ordered upload',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const SERIAL_SEND_DESCRIPTOR = msg({
|
||||
message: 'Serial send',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const ENSURES_FILES_ARE_SENT_IN_THE_ORDER_THEY_WERE_ADDED_DESCRIPTOR = msg({
|
||||
message: 'Ensures file messages appear in the order you sent them',
|
||||
comment: 'Settings search entry description. One-line summary of what the settings search entry controls.',
|
||||
});
|
||||
const START_UPLOADING_ATTACHMENTS_WHEN_THEY_ARE_ADDED_DESCRIPTOR = msg({
|
||||
message: 'Start uploading attachments as soon as they are added to the message input',
|
||||
comment: 'Settings search entry description. One-line summary of what the settings search entry controls.',
|
||||
@@ -711,4 +735,20 @@ export const chatSettingsIndex: Array<SearchableSettingDescriptor> = [
|
||||
audience: 'advanced',
|
||||
tags: ['chat'],
|
||||
},
|
||||
{
|
||||
id: 'chat-settings-sequential-file-send',
|
||||
tabType: 'advanced_settings',
|
||||
sectionId: 'chat',
|
||||
label: SEQUENTIAL_FILE_SEND_DESCRIPTOR,
|
||||
keywords: [
|
||||
SEQUENTIAL_SEND_DESCRIPTOR,
|
||||
FILE_ORDERING_DESCRIPTOR,
|
||||
ORDERED_UPLOAD_DESCRIPTOR,
|
||||
SERIAL_SEND_DESCRIPTOR,
|
||||
FILE_UPLOAD_DESCRIPTOR,
|
||||
],
|
||||
description: ENSURES_FILES_ARE_SENT_IN_THE_ORDER_THEY_WERE_ADDED_DESCRIPTOR,
|
||||
audience: 'advanced',
|
||||
tags: ['chat'],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -76,6 +76,7 @@ message AccessibilitySettings {
|
||||
optional double compact_message_group_spacing = 60;
|
||||
optional bool scroll_to_bottom_on_message_send = 61;
|
||||
optional bool dim_strikethrough_text = 62;
|
||||
optional bool sequential_file_send = 63;
|
||||
}
|
||||
|
||||
message AccessibilityOverrides {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user