From de56e3fd810dbdc802118d795b316519aea31696 Mon Sep 17 00:00:00 2001 From: Mizarc Date: Thu, 2 Jul 2026 11:35:03 +1000 Subject: [PATCH] feat(app): toggle for sequential msg file send order (#1245) Co-authored-by: Hampus --- .../accessibility/state/Accessibility.ts | 8 ++ .../messaging/commands/MessageCommands.tsx | 73 ++++++++++++++++++- .../tabs/AdvancedSettingDirectControls.tsx | 7 ++ .../AdvancedChatControls.tsx | 17 +++++ .../search_index/ChatSettingsIndex.ts | 40 ++++++++++ .../user/preferences/v1/accessibility.proto | 1 + .../user/preferences/v1/accessibility_pb.ts | 7 +- 7 files changed, 150 insertions(+), 3 deletions(-) diff --git a/fluxer_app/src/features/accessibility/state/Accessibility.ts b/fluxer_app/src/features/accessibility/state/Accessibility.ts index fe9a47654..21e935bef 100644 --- a/fluxer_app/src/features/accessibility/state/Accessibility.ts +++ b/fluxer_app/src/features/accessibility/state/Accessibility.ts @@ -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, diff --git a/fluxer_app/src/features/messaging/commands/MessageCommands.tsx b/fluxer_app/src/features/messaging/commands/MessageCommands.tsx index cb0458411..96a8363ed 100644 --- a/fluxer_app/src/features/messaging/commands/MessageCommands.tsx +++ b/fluxer_app/src/features/messaging/commands/MessageCommands.tsx @@ -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 | undefined>; + resolve: (value: RestResponse | undefined) => void; +} + +interface ChannelSendOrderState { + nextOrder: number; + nextExpected: number; + pending: Map; + processing: boolean; + channelId: string; +} + +const channelSendOrders = new Map(); + +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 | undefined>, +): Promise | undefined> { + return new Promise | undefined>((resolve) => { + const state = getOrCreateChannelState(channelId); + state.pending.set(order, {task, resolve}); + void processSequentialQueue(state); + }); +} + +async function processSequentialQueue(state: ChannelSendOrderState): Promise { + 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 { 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}); diff --git a/fluxer_app/src/features/user/components/modals/tabs/AdvancedSettingDirectControls.tsx b/fluxer_app/src/features/user/components/modals/tabs/AdvancedSettingDirectControls.tsx index c44ce44e0..c4ed1a9c8 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/AdvancedSettingDirectControls.tsx +++ b/fluxer_app/src/features/user/components/modals/tabs/AdvancedSettingDirectControls.tsx @@ -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 ( ); + case 'chat-settings-sequential-file-send': + return ( + + ); case 'chat-settings-scroll-to-bottom-on-send': return ( diff --git a/fluxer_app/src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedChatControls.tsx b/fluxer_app/src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedChatControls.tsx index 99e7fa7b0..5dfd50a01 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedChatControls.tsx +++ b/fluxer_app/src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedChatControls.tsx @@ -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 ( + AccessibilityCommands.update({sequentialFileSend: value})} + compact + data-flx="user.advanced-settings-tab.switch.sequential-file-send" + /> + ); +}); + export const ScrollToBottomOnSendControl = observer(() => { const {i18n} = useLingui(); return ( diff --git a/fluxer_app/src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts b/fluxer_app/src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts index f53aeb9cf..9939a6e32 100644 --- a/fluxer_app/src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts +++ b/fluxer_app/src/features/user/components/settings_utils/search_index/ChatSettingsIndex.ts @@ -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 = [ 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'], + }, ]; diff --git a/packages/schema/proto/fluxer/user/preferences/v1/accessibility.proto b/packages/schema/proto/fluxer/user/preferences/v1/accessibility.proto index 5ad9fb637..aa2e31252 100644 --- a/packages/schema/proto/fluxer/user/preferences/v1/accessibility.proto +++ b/packages/schema/proto/fluxer/user/preferences/v1/accessibility.proto @@ -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 { diff --git a/packages/schema/src/gen/fluxer/user/preferences/v1/accessibility_pb.ts b/packages/schema/src/gen/fluxer/user/preferences/v1/accessibility_pb.ts index d54256b1d..03b082215 100644 --- a/packages/schema/src/gen/fluxer/user/preferences/v1/accessibility_pb.ts +++ b/packages/schema/src/gen/fluxer/user/preferences/v1/accessibility_pb.ts @@ -6,7 +6,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file fluxer/user/preferences/v1/accessibility.proto. */ export const file_fluxer_user_preferences_v1_accessibility: GenFile = /*@__PURE__*/ - fileDesc("Ci5mbHV4ZXIvdXNlci9wcmVmZXJlbmNlcy92MS9hY2Nlc3NpYmlsaXR5LnByb3RvEhpmbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MSLjIQoVQWNjZXNzaWJpbGl0eVNldHRpbmdzEh4KEXNhdHVyYXRpb25fZmFjdG9yGAEgASgBSACIAQESHgoWYWx3YXlzX3VuZGVybGluZV9saW5rcxgCIAEoCBIiChVlbmFibGVfdGV4dF9zZWxlY3Rpb24YAyABKAhIAYgBARIlChhzaG93X21lc3NhZ2Vfc2VuZF9idXR0b24YBCABKAhIAogBARIlChhzaG93X3RleHRhcmVhX2ZvY3VzX3JpbmcYBSABKAhIA4gBARIbChNoaWRlX2tleWJvYXJkX2hpbnRzGAYgASgIEicKGmVzY2FwZV9leGl0c19rZXlib2FyZF9tb2RlGAcgASgISASIAQESLAofc3luY19yZWR1Y2VkX21vdGlvbl93aXRoX3N5c3RlbRgIIAEoCEgFiAEBEiQKF3JlZHVjZWRfbW90aW9uX292ZXJyaWRlGAkgASgISAaIAQESIgoVbWVzc2FnZV9ncm91cF9zcGFjaW5nGAogASgBSAeIAQESGwoObWVzc2FnZV9ndXR0ZXIYCyABKAFICIgBARIWCglmb250X3NpemUYDCABKAFICYgBARIuCiFzaG93X3VzZXJfYXZhdGFyc19pbl9jb21wYWN0X21vZGUYDSABKAhICogBARIrCiNtb2JpbGVfc3RpY2tlcl9hbmltYXRpb25fb3ZlcnJpZGRlbhgOIAEoCBImCh5tb2JpbGVfZ2lmX2F1dG9wbGF5X292ZXJyaWRkZW4YDyABKAgSJwofbW9iaWxlX2FuaW1hdGVfZW1vamlfb3ZlcnJpZGRlbhgQIAEoCBIrCh5tb2JpbGVfc3RpY2tlcl9hbmltYXRpb25fdmFsdWUYESABKAVIC4gBARImChltb2JpbGVfZ2lmX2F1dG9wbGF5X3ZhbHVlGBIgASgISAyIAQESJwoabW9iaWxlX2FuaW1hdGVfZW1vamlfdmFsdWUYEyABKAhIDYgBARIcChRhdXRvX3NlbmRfa2xpcHlfZ2lmcxgUIAEoCBIcCg9zaG93X2dpZl9idXR0b24YFSABKAhIDogBARIeChFzaG93X21lbWVzX2J1dHRvbhgWIAEoCEgPiAEBEiEKFHNob3dfc3RpY2tlcnNfYnV0dG9uGBcgASgISBCIAQESHgoRc2hvd19lbW9qaV9idXR0b24YGCABKAhIEYgBARInChpzaG93X21lZGlhX2Zhdm9yaXRlX2J1dHRvbhgZIAEoCEgSiAEBEicKGnNob3dfbWVkaWFfZG93bmxvYWRfYnV0dG9uGBogASgISBOIAQESJQoYc2hvd19tZWRpYV9kZWxldGVfYnV0dG9uGBsgASgISBSIAQESKAobc2hvd19zdXBwcmVzc19lbWJlZHNfYnV0dG9uGBwgASgISBWIAQESHwoSc2hvd19naWZfaW5kaWNhdG9yGB0gASgISBaIAQESLQogc2hvd19hdHRhY2htZW50X2V4cGlyeV9pbmRpY2F0b3IYHiABKAhIF4gBARIvCiJ1c2VfYnJvd3Nlcl9sb2NhbGVfZm9yX3RpbWVfZm9ybWF0GB8gASgISBiIAQESXQodY2hhbm5lbF90eXBpbmdfaW5kaWNhdG9yX21vZGUYICABKA4yNi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5DaGFubmVsVHlwaW5nSW5kaWNhdG9yTW9kZRIzCiZzaG93X3NlbGVjdGVkX2NoYW5uZWxfdHlwaW5nX2luZGljYXRvchghIAEoCEgZiAEBEiQKF3Nob3dfbWVzc2FnZV9hY3Rpb25fYmFyGCIgASgISBqIAQESNAonc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJfcXVpY2tfcmVhY3Rpb25zGCMgASgISBuIAQESMQokc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJfc2hpZnRfZXhwYW5kGCQgASgISByIAQESNQooc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJfb25seV9tb3JlX2J1dHRvbhglIAEoCEgdiAEBEjAKI3Nob3dfZGVmYXVsdF9lbW9qaXNfaW5fYXV0b2NvbXBsZXRlGCYgASgISB6IAQESLwoic2hvd19jdXN0b21fZW1vamlzX2luX2F1dG9jb21wbGV0ZRgnIAEoCEgfiAEBEioKHXNob3dfc3RpY2tlcnNfaW5fYXV0b2NvbXBsZXRlGCggASgISCCIAQESJwoac2hvd19tZW1lc19pbl9hdXRvY29tcGxldGUYKSABKAhIIYgBARJXCh9hdHRhY2htZW50X21lZGlhX2RpbWVuc2lvbl9zaXplGCogASgOMi4uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTWVkaWFEaW1lbnNpb25TaXplElIKGmVtYmVkX21lZGlhX2RpbWVuc2lvbl9zaXplGCsgASgOMi4uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTWVkaWFEaW1lbnNpb25TaXplEjUKKHZvaWNlX2NoYW5uZWxfam9pbl9yZXF1aXJlc19kb3VibGVfY2xpY2sYLCABKAhIIogBARIdChBjdXN0b21fdGhlbWVfY3NzGC0gASgJSCOIAQESGwoOc2hvd19mYXZvcml0ZXMYLiABKAhIJIgBARIXCgp6b29tX2xldmVsGC8gASgBSCWIAQESUQoXZG1fbWVzc2FnZV9wcmV2aWV3X21vZGUYMCABKA4yMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5EbU1lc3NhZ2VQcmV2aWV3TW9kZRIfChJlbmFibGVfdHRzX2NvbW1hbmQYMSABKAhIJogBARIVCgh0dHNfcmF0ZRgyIAEoAUgniAEBEjAKI3Nob3dfZmFkZWRfdW5yZWFkX29uX211dGVkX2NoYW5uZWxzGDMgASgISCiIAQESKAobc2hvd19jb250ZXh0X21lbnVfc2hvcnRjdXRzGDQgASgISCmIAQESKgodY29uZmlybV9iZWZvcmVfc3RhcnRpbmdfY2FsbHMYNSABKAhIKogBARJEChBoZHJfZGlzcGxheV9tb2RlGDYgASgOMiouZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSGRyRGlzcGxheU1vZGUSIAoTcHJlc2VydmVfZWRpdF9kcmFmdBg3IAEoCEgriAEBEiwKH3N0YXlfaW50ZXJhY3RpdmVfd2hlbl91bmZvY3VzZWQYOCABKAhILIgBARIyCiVjb25maXJtX2JlZm9yZV9qb2luaW5nX3ZvaWNlX2NoYW5uZWxzGDkgASgISC2IAQESMAojc2NyZWVuX3JlYWRlcl9hbm5vdW5jZV9uZXdfbWVzc2FnZXMYOiABKAhILogBARI0CidmaXJzdF9jbGlja19wYXNzX3Rocm91Z2hfd2hlbl91bmZvY3VzZWQYOyABKAhIL4gBARIqCh1jb21wYWN0X21lc3NhZ2VfZ3JvdXBfc3BhY2luZxg8IAEoAUgwiAEBEi0KIHNjcm9sbF90b19ib3R0b21fb25fbWVzc2FnZV9zZW5kGD0gASgISDGIAQESIwoWZGltX3N0cmlrZXRocm91Z2hfdGV4dBg+IAEoCEgyiAEBQhQKEl9zYXR1cmF0aW9uX2ZhY3RvckIYChZfZW5hYmxlX3RleHRfc2VsZWN0aW9uQhsKGV9zaG93X21lc3NhZ2Vfc2VuZF9idXR0b25CGwoZX3Nob3dfdGV4dGFyZWFfZm9jdXNfcmluZ0IdChtfZXNjYXBlX2V4aXRzX2tleWJvYXJkX21vZGVCIgogX3N5bmNfcmVkdWNlZF9tb3Rpb25fd2l0aF9zeXN0ZW1CGgoYX3JlZHVjZWRfbW90aW9uX292ZXJyaWRlQhgKFl9tZXNzYWdlX2dyb3VwX3NwYWNpbmdCEQoPX21lc3NhZ2VfZ3V0dGVyQgwKCl9mb250X3NpemVCJAoiX3Nob3dfdXNlcl9hdmF0YXJzX2luX2NvbXBhY3RfbW9kZUIhCh9fbW9iaWxlX3N0aWNrZXJfYW5pbWF0aW9uX3ZhbHVlQhwKGl9tb2JpbGVfZ2lmX2F1dG9wbGF5X3ZhbHVlQh0KG19tb2JpbGVfYW5pbWF0ZV9lbW9qaV92YWx1ZUISChBfc2hvd19naWZfYnV0dG9uQhQKEl9zaG93X21lbWVzX2J1dHRvbkIXChVfc2hvd19zdGlja2Vyc19idXR0b25CFAoSX3Nob3dfZW1vamlfYnV0dG9uQh0KG19zaG93X21lZGlhX2Zhdm9yaXRlX2J1dHRvbkIdChtfc2hvd19tZWRpYV9kb3dubG9hZF9idXR0b25CGwoZX3Nob3dfbWVkaWFfZGVsZXRlX2J1dHRvbkIeChxfc2hvd19zdXBwcmVzc19lbWJlZHNfYnV0dG9uQhUKE19zaG93X2dpZl9pbmRpY2F0b3JCIwohX3Nob3dfYXR0YWNobWVudF9leHBpcnlfaW5kaWNhdG9yQiUKI191c2VfYnJvd3Nlcl9sb2NhbGVfZm9yX3RpbWVfZm9ybWF0QikKJ19zaG93X3NlbGVjdGVkX2NoYW5uZWxfdHlwaW5nX2luZGljYXRvckIaChhfc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJCKgooX3Nob3dfbWVzc2FnZV9hY3Rpb25fYmFyX3F1aWNrX3JlYWN0aW9uc0InCiVfc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJfc2hpZnRfZXhwYW5kQisKKV9zaG93X21lc3NhZ2VfYWN0aW9uX2Jhcl9vbmx5X21vcmVfYnV0dG9uQiYKJF9zaG93X2RlZmF1bHRfZW1vamlzX2luX2F1dG9jb21wbGV0ZUIlCiNfc2hvd19jdXN0b21fZW1vamlzX2luX2F1dG9jb21wbGV0ZUIgCh5fc2hvd19zdGlja2Vyc19pbl9hdXRvY29tcGxldGVCHQobX3Nob3dfbWVtZXNfaW5fYXV0b2NvbXBsZXRlQisKKV92b2ljZV9jaGFubmVsX2pvaW5fcmVxdWlyZXNfZG91YmxlX2NsaWNrQhMKEV9jdXN0b21fdGhlbWVfY3NzQhEKD19zaG93X2Zhdm9yaXRlc0INCgtfem9vbV9sZXZlbEIVChNfZW5hYmxlX3R0c19jb21tYW5kQgsKCV90dHNfcmF0ZUImCiRfc2hvd19mYWRlZF91bnJlYWRfb25fbXV0ZWRfY2hhbm5lbHNCHgocX3Nob3dfY29udGV4dF9tZW51X3Nob3J0Y3V0c0IgCh5fY29uZmlybV9iZWZvcmVfc3RhcnRpbmdfY2FsbHNCFgoUX3ByZXNlcnZlX2VkaXRfZHJhZnRCIgogX3N0YXlfaW50ZXJhY3RpdmVfd2hlbl91bmZvY3VzZWRCKAomX2NvbmZpcm1fYmVmb3JlX2pvaW5pbmdfdm9pY2VfY2hhbm5lbHNCJgokX3NjcmVlbl9yZWFkZXJfYW5ub3VuY2VfbmV3X21lc3NhZ2VzQioKKF9maXJzdF9jbGlja19wYXNzX3Rocm91Z2hfd2hlbl91bmZvY3VzZWRCIAoeX2NvbXBhY3RfbWVzc2FnZV9ncm91cF9zcGFjaW5nQiMKIV9zY3JvbGxfdG9fYm90dG9tX29uX21lc3NhZ2Vfc2VuZEIZChdfZGltX3N0cmlrZXRocm91Z2hfdGV4dCJxChZBY2Nlc3NpYmlsaXR5T3ZlcnJpZGVzEhoKEmdpZl9hdXRvcGxheV9kaXJ0eRgBIAEoCBIbChNhbmltYXRlX2Vtb2ppX2RpcnR5GAIgASgIEh4KFmFuaW1hdGVfc3RpY2tlcnNfZGlydHkYAyABKAgq0gEKGkNoYW5uZWxUeXBpbmdJbmRpY2F0b3JNb2RlEi0KKUNIQU5ORUxfVFlQSU5HX0lORElDQVRPUl9NT0RFX1VOU1BFQ0lGSUVEEAASKQolQ0hBTk5FTF9UWVBJTkdfSU5ESUNBVE9SX01PREVfQVZBVEFSUxABEjAKLENIQU5ORUxfVFlQSU5HX0lORElDQVRPUl9NT0RFX0lORElDQVRPUl9PTkxZEAISKAokQ0hBTk5FTF9UWVBJTkdfSU5ESUNBVE9SX01PREVfSElEREVOEAMqegoSTWVkaWFEaW1lbnNpb25TaXplEiQKIE1FRElBX0RJTUVOU0lPTl9TSVpFX1VOU1BFQ0lGSUVEEAASHgoaTUVESUFfRElNRU5TSU9OX1NJWkVfU01BTEwQARIeChpNRURJQV9ESU1FTlNJT05fU0laRV9MQVJHRRACKqsBChREbU1lc3NhZ2VQcmV2aWV3TW9kZRInCiNETV9NRVNTQUdFX1BSRVZJRVdfTU9ERV9VTlNQRUNJRklFRBAAEh8KG0RNX01FU1NBR0VfUFJFVklFV19NT0RFX0FMTBABEicKI0RNX01FU1NBR0VfUFJFVklFV19NT0RFX1VOUkVBRF9PTkxZEAISIAocRE1fTUVTU0FHRV9QUkVWSUVXX01PREVfTk9ORRADKmwKDkhkckRpc3BsYXlNb2RlEiAKHEhEUl9ESVNQTEFZX01PREVfVU5TUEVDSUZJRUQQABIZChVIRFJfRElTUExBWV9NT0RFX0ZVTEwQARIdChlIRFJfRElTUExBWV9NT0RFX1NUQU5EQVJEEAJiBnByb3RvMw"); + fileDesc("Ci5mbHV4ZXIvdXNlci9wcmVmZXJlbmNlcy92MS9hY2Nlc3NpYmlsaXR5LnByb3RvEhpmbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MSKfIgoVQWNjZXNzaWJpbGl0eVNldHRpbmdzEh4KEXNhdHVyYXRpb25fZmFjdG9yGAEgASgBSACIAQESHgoWYWx3YXlzX3VuZGVybGluZV9saW5rcxgCIAEoCBIiChVlbmFibGVfdGV4dF9zZWxlY3Rpb24YAyABKAhIAYgBARIlChhzaG93X21lc3NhZ2Vfc2VuZF9idXR0b24YBCABKAhIAogBARIlChhzaG93X3RleHRhcmVhX2ZvY3VzX3JpbmcYBSABKAhIA4gBARIbChNoaWRlX2tleWJvYXJkX2hpbnRzGAYgASgIEicKGmVzY2FwZV9leGl0c19rZXlib2FyZF9tb2RlGAcgASgISASIAQESLAofc3luY19yZWR1Y2VkX21vdGlvbl93aXRoX3N5c3RlbRgIIAEoCEgFiAEBEiQKF3JlZHVjZWRfbW90aW9uX292ZXJyaWRlGAkgASgISAaIAQESIgoVbWVzc2FnZV9ncm91cF9zcGFjaW5nGAogASgBSAeIAQESGwoObWVzc2FnZV9ndXR0ZXIYCyABKAFICIgBARIWCglmb250X3NpemUYDCABKAFICYgBARIuCiFzaG93X3VzZXJfYXZhdGFyc19pbl9jb21wYWN0X21vZGUYDSABKAhICogBARIrCiNtb2JpbGVfc3RpY2tlcl9hbmltYXRpb25fb3ZlcnJpZGRlbhgOIAEoCBImCh5tb2JpbGVfZ2lmX2F1dG9wbGF5X292ZXJyaWRkZW4YDyABKAgSJwofbW9iaWxlX2FuaW1hdGVfZW1vamlfb3ZlcnJpZGRlbhgQIAEoCBIrCh5tb2JpbGVfc3RpY2tlcl9hbmltYXRpb25fdmFsdWUYESABKAVIC4gBARImChltb2JpbGVfZ2lmX2F1dG9wbGF5X3ZhbHVlGBIgASgISAyIAQESJwoabW9iaWxlX2FuaW1hdGVfZW1vamlfdmFsdWUYEyABKAhIDYgBARIcChRhdXRvX3NlbmRfa2xpcHlfZ2lmcxgUIAEoCBIcCg9zaG93X2dpZl9idXR0b24YFSABKAhIDogBARIeChFzaG93X21lbWVzX2J1dHRvbhgWIAEoCEgPiAEBEiEKFHNob3dfc3RpY2tlcnNfYnV0dG9uGBcgASgISBCIAQESHgoRc2hvd19lbW9qaV9idXR0b24YGCABKAhIEYgBARInChpzaG93X21lZGlhX2Zhdm9yaXRlX2J1dHRvbhgZIAEoCEgSiAEBEicKGnNob3dfbWVkaWFfZG93bmxvYWRfYnV0dG9uGBogASgISBOIAQESJQoYc2hvd19tZWRpYV9kZWxldGVfYnV0dG9uGBsgASgISBSIAQESKAobc2hvd19zdXBwcmVzc19lbWJlZHNfYnV0dG9uGBwgASgISBWIAQESHwoSc2hvd19naWZfaW5kaWNhdG9yGB0gASgISBaIAQESLQogc2hvd19hdHRhY2htZW50X2V4cGlyeV9pbmRpY2F0b3IYHiABKAhIF4gBARIvCiJ1c2VfYnJvd3Nlcl9sb2NhbGVfZm9yX3RpbWVfZm9ybWF0GB8gASgISBiIAQESXQodY2hhbm5lbF90eXBpbmdfaW5kaWNhdG9yX21vZGUYICABKA4yNi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5DaGFubmVsVHlwaW5nSW5kaWNhdG9yTW9kZRIzCiZzaG93X3NlbGVjdGVkX2NoYW5uZWxfdHlwaW5nX2luZGljYXRvchghIAEoCEgZiAEBEiQKF3Nob3dfbWVzc2FnZV9hY3Rpb25fYmFyGCIgASgISBqIAQESNAonc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJfcXVpY2tfcmVhY3Rpb25zGCMgASgISBuIAQESMQokc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJfc2hpZnRfZXhwYW5kGCQgASgISByIAQESNQooc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJfb25seV9tb3JlX2J1dHRvbhglIAEoCEgdiAEBEjAKI3Nob3dfZGVmYXVsdF9lbW9qaXNfaW5fYXV0b2NvbXBsZXRlGCYgASgISB6IAQESLwoic2hvd19jdXN0b21fZW1vamlzX2luX2F1dG9jb21wbGV0ZRgnIAEoCEgfiAEBEioKHXNob3dfc3RpY2tlcnNfaW5fYXV0b2NvbXBsZXRlGCggASgISCCIAQESJwoac2hvd19tZW1lc19pbl9hdXRvY29tcGxldGUYKSABKAhIIYgBARJXCh9hdHRhY2htZW50X21lZGlhX2RpbWVuc2lvbl9zaXplGCogASgOMi4uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTWVkaWFEaW1lbnNpb25TaXplElIKGmVtYmVkX21lZGlhX2RpbWVuc2lvbl9zaXplGCsgASgOMi4uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTWVkaWFEaW1lbnNpb25TaXplEjUKKHZvaWNlX2NoYW5uZWxfam9pbl9yZXF1aXJlc19kb3VibGVfY2xpY2sYLCABKAhIIogBARIdChBjdXN0b21fdGhlbWVfY3NzGC0gASgJSCOIAQESGwoOc2hvd19mYXZvcml0ZXMYLiABKAhIJIgBARIXCgp6b29tX2xldmVsGC8gASgBSCWIAQESUQoXZG1fbWVzc2FnZV9wcmV2aWV3X21vZGUYMCABKA4yMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5EbU1lc3NhZ2VQcmV2aWV3TW9kZRIfChJlbmFibGVfdHRzX2NvbW1hbmQYMSABKAhIJogBARIVCgh0dHNfcmF0ZRgyIAEoAUgniAEBEjAKI3Nob3dfZmFkZWRfdW5yZWFkX29uX211dGVkX2NoYW5uZWxzGDMgASgISCiIAQESKAobc2hvd19jb250ZXh0X21lbnVfc2hvcnRjdXRzGDQgASgISCmIAQESKgodY29uZmlybV9iZWZvcmVfc3RhcnRpbmdfY2FsbHMYNSABKAhIKogBARJEChBoZHJfZGlzcGxheV9tb2RlGDYgASgOMiouZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSGRyRGlzcGxheU1vZGUSIAoTcHJlc2VydmVfZWRpdF9kcmFmdBg3IAEoCEgriAEBEiwKH3N0YXlfaW50ZXJhY3RpdmVfd2hlbl91bmZvY3VzZWQYOCABKAhILIgBARIyCiVjb25maXJtX2JlZm9yZV9qb2luaW5nX3ZvaWNlX2NoYW5uZWxzGDkgASgISC2IAQESMAojc2NyZWVuX3JlYWRlcl9hbm5vdW5jZV9uZXdfbWVzc2FnZXMYOiABKAhILogBARI0CidmaXJzdF9jbGlja19wYXNzX3Rocm91Z2hfd2hlbl91bmZvY3VzZWQYOyABKAhIL4gBARIqCh1jb21wYWN0X21lc3NhZ2VfZ3JvdXBfc3BhY2luZxg8IAEoAUgwiAEBEi0KIHNjcm9sbF90b19ib3R0b21fb25fbWVzc2FnZV9zZW5kGD0gASgISDGIAQESIwoWZGltX3N0cmlrZXRocm91Z2hfdGV4dBg+IAEoCEgyiAEBEiEKFHNlcXVlbnRpYWxfZmlsZV9zZW5kGD8gASgISDOIAQFCFAoSX3NhdHVyYXRpb25fZmFjdG9yQhgKFl9lbmFibGVfdGV4dF9zZWxlY3Rpb25CGwoZX3Nob3dfbWVzc2FnZV9zZW5kX2J1dHRvbkIbChlfc2hvd190ZXh0YXJlYV9mb2N1c19yaW5nQh0KG19lc2NhcGVfZXhpdHNfa2V5Ym9hcmRfbW9kZUIiCiBfc3luY19yZWR1Y2VkX21vdGlvbl93aXRoX3N5c3RlbUIaChhfcmVkdWNlZF9tb3Rpb25fb3ZlcnJpZGVCGAoWX21lc3NhZ2VfZ3JvdXBfc3BhY2luZ0IRCg9fbWVzc2FnZV9ndXR0ZXJCDAoKX2ZvbnRfc2l6ZUIkCiJfc2hvd191c2VyX2F2YXRhcnNfaW5fY29tcGFjdF9tb2RlQiEKH19tb2JpbGVfc3RpY2tlcl9hbmltYXRpb25fdmFsdWVCHAoaX21vYmlsZV9naWZfYXV0b3BsYXlfdmFsdWVCHQobX21vYmlsZV9hbmltYXRlX2Vtb2ppX3ZhbHVlQhIKEF9zaG93X2dpZl9idXR0b25CFAoSX3Nob3dfbWVtZXNfYnV0dG9uQhcKFV9zaG93X3N0aWNrZXJzX2J1dHRvbkIUChJfc2hvd19lbW9qaV9idXR0b25CHQobX3Nob3dfbWVkaWFfZmF2b3JpdGVfYnV0dG9uQh0KG19zaG93X21lZGlhX2Rvd25sb2FkX2J1dHRvbkIbChlfc2hvd19tZWRpYV9kZWxldGVfYnV0dG9uQh4KHF9zaG93X3N1cHByZXNzX2VtYmVkc19idXR0b25CFQoTX3Nob3dfZ2lmX2luZGljYXRvckIjCiFfc2hvd19hdHRhY2htZW50X2V4cGlyeV9pbmRpY2F0b3JCJQojX3VzZV9icm93c2VyX2xvY2FsZV9mb3JfdGltZV9mb3JtYXRCKQonX3Nob3dfc2VsZWN0ZWRfY2hhbm5lbF90eXBpbmdfaW5kaWNhdG9yQhoKGF9zaG93X21lc3NhZ2VfYWN0aW9uX2JhckIqCihfc2hvd19tZXNzYWdlX2FjdGlvbl9iYXJfcXVpY2tfcmVhY3Rpb25zQicKJV9zaG93X21lc3NhZ2VfYWN0aW9uX2Jhcl9zaGlmdF9leHBhbmRCKwopX3Nob3dfbWVzc2FnZV9hY3Rpb25fYmFyX29ubHlfbW9yZV9idXR0b25CJgokX3Nob3dfZGVmYXVsdF9lbW9qaXNfaW5fYXV0b2NvbXBsZXRlQiUKI19zaG93X2N1c3RvbV9lbW9qaXNfaW5fYXV0b2NvbXBsZXRlQiAKHl9zaG93X3N0aWNrZXJzX2luX2F1dG9jb21wbGV0ZUIdChtfc2hvd19tZW1lc19pbl9hdXRvY29tcGxldGVCKwopX3ZvaWNlX2NoYW5uZWxfam9pbl9yZXF1aXJlc19kb3VibGVfY2xpY2tCEwoRX2N1c3RvbV90aGVtZV9jc3NCEQoPX3Nob3dfZmF2b3JpdGVzQg0KC196b29tX2xldmVsQhUKE19lbmFibGVfdHRzX2NvbW1hbmRCCwoJX3R0c19yYXRlQiYKJF9zaG93X2ZhZGVkX3VucmVhZF9vbl9tdXRlZF9jaGFubmVsc0IeChxfc2hvd19jb250ZXh0X21lbnVfc2hvcnRjdXRzQiAKHl9jb25maXJtX2JlZm9yZV9zdGFydGluZ19jYWxsc0IWChRfcHJlc2VydmVfZWRpdF9kcmFmdEIiCiBfc3RheV9pbnRlcmFjdGl2ZV93aGVuX3VuZm9jdXNlZEIoCiZfY29uZmlybV9iZWZvcmVfam9pbmluZ192b2ljZV9jaGFubmVsc0ImCiRfc2NyZWVuX3JlYWRlcl9hbm5vdW5jZV9uZXdfbWVzc2FnZXNCKgooX2ZpcnN0X2NsaWNrX3Bhc3NfdGhyb3VnaF93aGVuX3VuZm9jdXNlZEIgCh5fY29tcGFjdF9tZXNzYWdlX2dyb3VwX3NwYWNpbmdCIwohX3Njcm9sbF90b19ib3R0b21fb25fbWVzc2FnZV9zZW5kQhkKF19kaW1fc3RyaWtldGhyb3VnaF90ZXh0QhcKFV9zZXF1ZW50aWFsX2ZpbGVfc2VuZCJxChZBY2Nlc3NpYmlsaXR5T3ZlcnJpZGVzEhoKEmdpZl9hdXRvcGxheV9kaXJ0eRgBIAEoCBIbChNhbmltYXRlX2Vtb2ppX2RpcnR5GAIgASgIEh4KFmFuaW1hdGVfc3RpY2tlcnNfZGlydHkYAyABKAgq0gEKGkNoYW5uZWxUeXBpbmdJbmRpY2F0b3JNb2RlEi0KKUNIQU5ORUxfVFlQSU5HX0lORElDQVRPUl9NT0RFX1VOU1BFQ0lGSUVEEAASKQolQ0hBTk5FTF9UWVBJTkdfSU5ESUNBVE9SX01PREVfQVZBVEFSUxABEjAKLENIQU5ORUxfVFlQSU5HX0lORElDQVRPUl9NT0RFX0lORElDQVRPUl9PTkxZEAISKAokQ0hBTk5FTF9UWVBJTkdfSU5ESUNBVE9SX01PREVfSElEREVOEAMqegoSTWVkaWFEaW1lbnNpb25TaXplEiQKIE1FRElBX0RJTUVOU0lPTl9TSVpFX1VOU1BFQ0lGSUVEEAASHgoaTUVESUFfRElNRU5TSU9OX1NJWkVfU01BTEwQARIeChpNRURJQV9ESU1FTlNJT05fU0laRV9MQVJHRRACKqsBChREbU1lc3NhZ2VQcmV2aWV3TW9kZRInCiNETV9NRVNTQUdFX1BSRVZJRVdfTU9ERV9VTlNQRUNJRklFRBAAEh8KG0RNX01FU1NBR0VfUFJFVklFV19NT0RFX0FMTBABEicKI0RNX01FU1NBR0VfUFJFVklFV19NT0RFX1VOUkVBRF9PTkxZEAISIAocRE1fTUVTU0FHRV9QUkVWSUVXX01PREVfTk9ORRADKmwKDkhkckRpc3BsYXlNb2RlEiAKHEhEUl9ESVNQTEFZX01PREVfVU5TUEVDSUZJRUQQABIZChVIRFJfRElTUExBWV9NT0RFX0ZVTEwQARIdChlIRFJfRElTUExBWV9NT0RFX1NUQU5EQVJEEAJiBnByb3RvMw"); /** * @generated from message fluxer.user.preferences.v1.AccessibilitySettings @@ -321,6 +321,11 @@ export type AccessibilitySettings = Message<"fluxer.user.preferences.v1.Accessib * @generated from field: optional bool dim_strikethrough_text = 62; */ dimStrikethroughText?: boolean | undefined; + + /** + * @generated from field: optional bool sequential_file_send = 63; + */ + sequentialFileSend?: boolean | undefined; }; /**