chore(deps): upgrade all dependencies, toolchains and images (#2825)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-17 21:08:56 +02:00
1 parent 56e04e7b53
commit 3b552e00ef
510 files changed
+23614 -37473

No files matched your search

@@ -61,7 +61,7 @@ function isFunctionNode(node: AstNode | null | undefined): boolean {
const REACT_HOOK_NAMESPACES = new Set(['React']);
function isReactHookNamespace(object: AstNode | null | undefined): boolean {
if (!object || object.type !== 'Identifier' || typeof object.name !== 'string') {
if (object?.type !== 'Identifier' || typeof object.name !== 'string') {
return false;
}
return REACT_HOOK_NAMESPACES.has(object.name);
@@ -181,7 +181,7 @@ function findHookOrderHazards(): Array<HookOrderFinding> {
const source = readFileSync(file, 'utf8');
const ast = parse(source, {
sourceType: 'module',
plugins: ['typescript', 'jsx', 'decorators-legacy'],
plugins: file.endsWith('.tsx') ? ['typescript', 'jsx'] : ['typescript'],
}) as unknown as AstNode;
walk(ast, (node, parent) => {
if (!isFunctionNode(node)) {
@@ -57,7 +57,7 @@ import type {MessageAttachment} from '@fluxer/schema/src/domains/message/Message
import type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {comparer, computed, type IComputedValue} from 'mobx';
import {compareStructural, computed, type IComputedValue} from 'mobx';
import {useEffect, useMemo, useState} from 'react';
const GUILD_MESSAGES_DISABLED_DESCRIPTOR = msg({
@@ -325,7 +325,7 @@ function buildForwardSearchBoostersFromStores(): ForwardSearchBoosters {
}
function createForwardSearchCandidates(i18n: I18n): IComputedValue<ForwardSearchCandidates> {
const options = {equals: comparer.structural};
const options = {equals: compareStructural};
const users = computed(buildForwardUserCandidates, options);
const groupDMs = computed(() => buildForwardGroupDMCandidates(i18n), options);
const channels = computed(buildForwardChannelCandidates, options);
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
import {action, makeAutoObservable} from 'mobx';
import {makeAutoObservable} from 'mobx';
const InitializationState = {
LOADING: 'LOADING',
@@ -41,32 +41,27 @@ class Initialization {
return this.state === InitializationState.READY;
}
@action
setLoading(): void {
this.state = InitializationState.LOADING;
this.error = null;
}
@action
setConnecting(): void {
this.state = InitializationState.CONNECTING;
this.error = null;
}
@action
setReady(): void {
this.state = InitializationState.READY;
this.hasCompletedInitialLoad = true;
this.error = null;
}
@action
setError(error: string): void {
this.state = InitializationState.ERROR;
this.error = error;
}
@action
reset(): void {
this.state = InitializationState.LOADING;
this.hasCompletedInitialLoad = false;
@@ -26,6 +26,8 @@ import type {InstanceConfigResponse} from '@fluxer/schema/src/domains/admin/Admi
import {makeAutoObservable, reaction, runInAction} from 'mobx';
export type {
GifProvider,
GifProviderInfo,
InstanceCaptcha,
InstanceCommunity,
InstanceDiscoveryResponse,
@@ -34,7 +36,6 @@ export type {
InstanceServices,
InstanceSsoConfig,
};
export type {GifProvider, GifProviderInfo};
export interface RuntimeConfigSnapshot {
apiEndpoint: string;
@@ -4,7 +4,7 @@ import ExperimentAssignments from '@app/features/experiment/state/ExperimentAssi
import SessionManager from '@app/features/platform/state/AuthSession';
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
import type {UserPrivate} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {action, computed, makeAutoObservable} from 'mobx';
import {computed, makeAutoObservable} from 'mobx';
const LoginState = {
Default: 'default',
@@ -67,18 +67,15 @@ class Authentication {
return SessionManager.userId;
}
@action
setUserId(userId: string | null): void {
SessionManager.setUserId(userId);
}
@action
handleGatewayReady({user}: {user: UserPrivate}): void {
SessionManager.setUserId(user.id);
SessionManager.handleConnectionReady();
}
@action
handleAuthSessionChange({token}: {token: string}): void {
SessionManager.setToken(token || null);
}
@@ -90,7 +87,6 @@ class Authentication {
}
}
@action
handleSessionStart({token}: {token: string | null | undefined}): void {
if (token) {
SessionManager.setToken(token);
@@ -102,7 +98,6 @@ class Authentication {
this.mfaMethods = null;
}
@action
handleMfaTicketSet({
ticket,
totp,
@@ -115,14 +110,12 @@ class Authentication {
this.mfaMethods = {totp, webauthn};
}
@action
handleMfaTicketClear(): void {
this.loginState = LoginState.Default;
this.mfaTicket = null;
this.mfaMethods = null;
}
@action
handleLogout(options?: {skipRedirect?: boolean}): void {
ExperimentAssignments.reset();
this.loginState = LoginState.Default;
@@ -530,7 +530,7 @@ export const ChannelHeader = observer(
if (hasCallableRecipients && !(isDM && isBotDMRecipient)) {
desktopLeadingActionCount += 2;
}
if (showPins && Boolean(channel)) {
if (showPins && channel) {
desktopLeadingActionCount += 1;
}
if (hasChannel && isFriendDM && !isGroupDM) {
@@ -177,26 +177,41 @@
.channelGrid[data-voice-text-split-view='true'] .compactCallWrapper {
flex: 0 0 auto;
height: clamp(13rem, 42vh, 28rem);
height: clamp(13rem, 42svh, 28rem);
max-height: 48vh;
max-height: 48svh;
}
@supports (height: 1svh) {
.channelGrid[data-voice-text-split-view='true'] .compactCallWrapper {
height: clamp(13rem, 42svh, 28rem);
max-height: 48svh;
}
}
@media (min-width: 600px) and (orientation: portrait) {
.channelGrid[data-voice-text-split-view='true'] .compactCallWrapper {
height: clamp(18rem, 44vh, 34rem);
height: clamp(18rem, 44svh, 34rem);
max-height: 50vh;
max-height: 50svh;
}
@supports (height: 1svh) {
.channelGrid[data-voice-text-split-view='true'] .compactCallWrapper {
height: clamp(18rem, 44svh, 34rem);
max-height: 50svh;
}
}
}
@media (max-height: 600px) and (orientation: portrait) {
.channelGrid[data-voice-text-split-view='true'] .compactCallWrapper {
height: clamp(9rem, 36vh, 13rem);
height: clamp(9rem, 36svh, 13rem);
max-height: 40vh;
max-height: 40svh;
}
@supports (height: 1svh) {
.channelGrid[data-voice-text-split-view='true'] .compactCallWrapper {
height: clamp(9rem, 36svh, 13rem);
max-height: 40svh;
}
}
}
@@ -3,7 +3,7 @@
import {getUnreadDividerBeforeMessageId} from '@app/features/channel/components/ChannelMessageStreamUtils';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import type {ChannelStreamItem} from '@app/features/messaging/utils/MessageGroupingUtils';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const GROUP_ITEMS = Array.from(
{length: 100},
@@ -29,8 +29,8 @@ const GROUPS = Array.from({length: 1_000}, (_value, groupIndex) =>
),
);
describe('ChannelMessageStreamUtils benchmarks', () => {
bench('find unread divider id across 1k pending render groups', () => {
test('ChannelMessageStreamUtils benchmarks', async ({bench}) => {
await bench('find unread divider id across 1k pending render groups', () => {
let hits = 0;
for (const group of GROUPS) {
if (getUnreadDividerBeforeMessageId(group) !== null) {
@@ -38,5 +38,5 @@ describe('ChannelMessageStreamUtils benchmarks', () => {
}
}
(globalThis as {__channelMessageStreamBenchSink?: number}).__channelMessageStreamBenchSink = hits;
});
}).run();
});
@@ -2,7 +2,7 @@
import {splitMediaAndFileAttachments} from '@app/features/channel/components/MessageAttachmentUtils';
import type {MessageAttachment} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const CONTENT_TYPES = [
'image/png',
@@ -30,8 +30,8 @@ const ATTACHMENTS = Array.from({length: 500}, (_value, index): MessageAttachment
};
});
describe('MessageAttachmentUtils benchmarks', () => {
bench('split 500 mixed media and file attachments', () => {
test('MessageAttachmentUtils benchmarks', async ({bench}) => {
await bench('split 500 mixed media and file attachments', () => {
splitMediaAndFileAttachments(ATTACHMENTS);
});
}).run();
});
@@ -21,7 +21,7 @@ export function getDmRouteChannelId(pathname: string): string | null {
}
export function canInviteInChannel(channel?: Channel | null): channel is Channel {
if (!channel || !channel.guildId) {
if (!channel?.guildId) {
return false;
}
return InviteUtils.canInviteToChannel(channel.id, channel.guildId);
@@ -196,7 +196,7 @@ export const EmbedYouTube: FC<EmbedYouTubeProps> = observer(({embed, width = YOU
() => decodeThumbHashDataURL(embed.thumbnail?.placeholder),
[embed.thumbnail?.placeholder],
);
if (!(embed.video && embed.thumbnail && embed.thumbnail.proxy_url)) {
if (!(embed.video && embed.thumbnail?.proxy_url)) {
return null;
}
const videoWidth = embed.video.width ?? YOUTUBE_CONFIG.DEFAULT_WIDTH;
@@ -459,7 +459,7 @@ const ChannelPermissionsTab: React.FC<{channelId: string}> = observer(({channelI
],
);
const isSyncedWithParent = useMemo(() => {
if (!channel || !channel.parentId || !parentChannel || !parentChannel.isGuildCategory()) return null;
if (!channel?.parentId || !parentChannel?.isGuildCategory()) return null;
const channelOverwrites = channel.permissionOverwrites || {};
const parentOverwrites = parentChannel.permissionOverwrites || {};
const channelKeys = Object.keys(channelOverwrites).sort();
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {buildGifPickerGridData} from '@app/features/channel/components/pickers/gif/GifPickerGridData';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
function gif(index: number, width = 200 + (index % 80), height = 120 + (index % 160)) {
return {
@@ -49,8 +49,8 @@ const FAVORITE_MEMES = Array.from({length: 100}, (_, index) => ({
url: `https://cdn.example.test/meme-${index}.mp4`,
}));
describe('GifPickerGridData benchmarks', () => {
bench('builds 500 GIF result items', () => {
test('GifPickerGridData benchmarks', async ({bench}) => {
await bench('builds 500 GIF result items', () => {
buildGifPickerGridData({
surface: 'results',
loading: false,
@@ -65,9 +65,9 @@ describe('GifPickerGridData benchmarks', () => {
favoriteTitle: 'Favorites',
trendingTitle: 'Trending',
});
});
}).run();
bench('builds 500 favorite GIF items', () => {
await bench('builds 500 favorite GIF items', () => {
buildGifPickerGridData({
surface: 'favorites',
loading: false,
@@ -82,9 +82,9 @@ describe('GifPickerGridData benchmarks', () => {
favoriteTitle: 'Favorites',
trendingTitle: 'Trending',
});
});
}).run();
bench('builds featured categories and preview tiles', () => {
await bench('builds featured categories and preview tiles', () => {
buildGifPickerGridData({
surface: 'featured',
loading: false,
@@ -99,5 +99,5 @@ describe('GifPickerGridData benchmarks', () => {
favoriteTitle: 'Favorites',
trendingTitle: 'Trending',
});
});
}).run();
});
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {buildGifPickerLoadingSkeletonLayout} from '@app/features/channel/components/pickers/gif/GifPickerLoadingSkeletonGridLayout';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const VIEWPORTS = [
{viewportWidth: 360, viewportHeight: 480},
@@ -10,10 +10,10 @@ const VIEWPORTS = [
{viewportWidth: 960, viewportHeight: 840},
] as const;
describe('GifPickerLoadingSkeletonGridLayout benchmarks', () => {
bench('builds deterministic masonry skeleton layouts for common picker sizes', () => {
test('GifPickerLoadingSkeletonGridLayout benchmarks', async ({bench}) => {
await bench('builds deterministic masonry skeleton layouts for common picker sizes', () => {
for (const viewport of VIEWPORTS) {
buildGifPickerLoadingSkeletonLayout(viewport);
}
});
}).run();
});
@@ -21,7 +21,7 @@ import type {Gif, GifFeatured} from '@app/features/expressions/commands/GifComma
import * as GifCommands from '@app/features/expressions/commands/GifCommands';
import type {DebouncedFunction} from '@app/features/platform/utils/scroll_manager/shared';
import debounce from 'lodash/debounce';
import {makeAutoObservable, observable, runInAction} from 'mobx';
import {makeAutoObservable, observableRef, runInAction} from 'mobx';
const EMPTY_FEATURED: GifFeatured = Object.freeze({categories: [], gifs: []});
const TRENDING_CACHE_KEY = 'trending';
@@ -83,7 +83,7 @@ export class GifPickerState {
suggestDebounced: false,
disposed: false,
requestSequence: false,
snapshot: observable.ref,
snapshot: observableRef,
categoryResultCache: false,
loadingSkeletonTimerId: false,
pendingSearchSkeletonMode: false,
@@ -6,12 +6,12 @@ import {
selectGifPickerModel,
transitionGifPickerSnapshot,
} from '@app/features/channel/components/pickers/gif/GifPickerStateMachine';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const SEARCH_TERMS = Array.from({length: 1_000}, (_, index) => `query-${index}`);
describe('GifPickerStateMachine benchmarks', () => {
bench('coalesces 1k search term transitions while preserving the visible surface', () => {
test('GifPickerStateMachine benchmarks', async ({bench}) => {
await bench('coalesces 1k search term transitions while preserving the visible surface', () => {
let snapshot = createGifPickerSnapshot();
snapshot = transitionGifPickerSnapshot(snapshot, {
type: 'gifPicker.featuredSucceeded',
@@ -25,9 +25,9 @@ describe('GifPickerStateMachine benchmarks', () => {
});
}
selectGifPickerModel(snapshot);
});
}).run();
bench('rejects 1k stale search completions', () => {
await bench('rejects 1k stale search completions', () => {
let snapshot = createGifPickerSnapshot();
for (let index = 0; index < SEARCH_TERMS.length; index += 1) {
const term = SEARCH_TERMS[index];
@@ -52,9 +52,9 @@ describe('GifPickerStateMachine benchmarks', () => {
});
}
selectGifPickerModel(snapshot);
});
}).run();
bench('commits latest search result after a fast query burst', () => {
await bench('commits latest search result after a fast query burst', () => {
let snapshot = createGifPickerSnapshot();
for (let index = 0; index < SEARCH_TERMS.length; index += 1) {
const term = SEARCH_TERMS[index];
@@ -77,9 +77,9 @@ describe('GifPickerStateMachine benchmarks', () => {
resultCount: 24,
});
selectGifPickerModel(snapshot);
});
}).run();
bench('rejects 1k stale skeleton timer events before showing the active one', () => {
await bench('rejects 1k stale skeleton timer events before showing the active one', () => {
const activeRequestId = createGifPickerRequestId(SEARCH_TERMS.length + 1);
let snapshot = createGifPickerSnapshot();
snapshot = transitionGifPickerSnapshot(snapshot, {
@@ -107,9 +107,9 @@ describe('GifPickerStateMachine benchmarks', () => {
term: 'active',
});
selectGifPickerModel(snapshot);
});
}).run();
bench('opens 1k cached category snapshots without entering loading', () => {
await bench('opens 1k cached category snapshots without entering loading', () => {
let snapshot = createGifPickerSnapshot();
for (const term of SEARCH_TERMS) {
snapshot = transitionGifPickerSnapshot(snapshot, {
@@ -119,5 +119,5 @@ describe('GifPickerStateMachine benchmarks', () => {
});
}
selectGifPickerModel(snapshot);
});
}).run();
});
@@ -19,7 +19,7 @@ import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
import {ME} from '@fluxer/constants/src/AppConstants';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import {ChannelFrecencyStateSchema} from '@fluxer/schema/src/gen/fluxer/user/preferences/v1/preferences_pb';
import {comparer, isObservableMap, makeAutoObservable, observable, reaction} from 'mobx';
import {compareShallow, isObservableMap, makeAutoObservable, observableShallow, reaction} from 'mobx';
const TRACKABLE_ID_PATTERN = /^\d{17,19}$/;
const FRECENCY_REFRESH_INTERVAL_MS = 3_600_000;
@@ -49,7 +49,7 @@ class ChannelFrecency {
usageHistory = new Map<string, ChannelFrecencyEntry>();
constructor() {
makeAutoObservable(this, {usageHistory: observable.shallow}, {autoBind: true});
makeAutoObservable(this, {usageHistory: observableShallow}, {autoBind: true});
void this.initPersistence();
}
@@ -72,7 +72,7 @@ class ChannelFrecency {
reaction(
() => [Navigation.guildId, Navigation.channelId] as const,
([guildId, channelId]) => this.recordSelection(guildId, channelId),
{equals: comparer.shallow, fireImmediately: true},
{equals: compareShallow, fireImmediately: true},
);
reaction(
() => [MediaEngine.guildId, MediaEngine.channelId] as const,
@@ -80,7 +80,7 @@ class ChannelFrecency {
if (channelId === null && MediaEngine.localDisconnectReason === 'channelMove') return;
this.recordSelection(guildId, channelId);
},
{equals: comparer.shallow},
{equals: compareShallow},
);
}
@@ -14,7 +14,7 @@ import SelectedGuild from '@app/features/navigation/state/SelectedGuild';
import type {SearchSegment} from '@app/features/search/utils/SearchSegmentManager';
import type {MessageSearchScope} from '@app/features/search/utils/SearchUtils';
import {ME} from '@fluxer/constants/src/AppConstants';
import {makeAutoObservable, observable} from 'mobx';
import {makeAutoObservable, observableRef, observableShallow} from 'mobx';
type ChannelSearchSortMode = 'newest' | 'oldest' | 'relevant';
@@ -38,7 +38,7 @@ class ChannelSearchContext {
constructor() {
makeAutoObservable(this, {
machineSnapshot: observable.ref,
machineSnapshot: observableRef,
});
}
@@ -52,7 +52,7 @@ class ChannelSearch {
constructor() {
makeAutoObservable<this, 'contexts'>(this, {
contexts: observable.shallow,
contexts: observableShallow,
});
}
@@ -16,7 +16,7 @@ import type {Channel as WireChannel} from '@fluxer/schema/src/domains/channel/Ch
import type {Message as WireMessage} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import type {UserPartial} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import * as SnowflakeUtils from '@fluxer/snowflake/src/SnowflakeUtils';
import {action, makeAutoObservable, observable} from 'mobx';
import {makeAutoObservable, observableRef, observableShallow} from 'mobx';
const EMPTY_CHANNELS: ReadonlyArray<Channel> = Object.freeze([]);
const sortDMs = (a: Channel, b: Channel) => {
@@ -58,7 +58,7 @@ class Channels {
constructor() {
makeAutoObservable<this, 'channelsByGuildId' | 'privateChannelList'>(
this,
{channelsByGuildId: observable.shallow, privateChannelList: observable.ref},
{channelsByGuildId: observableShallow, privateChannelList: observableRef},
{autoBind: true},
);
}
@@ -98,7 +98,6 @@ class Channels {
return this.privateChannelList;
}
@action
removeChannelOptimistically(channelId: string): void {
if (this.optimisticChannelBackups.has(channelId)) {
return;
@@ -111,7 +110,6 @@ class Channels {
this.deleteChannelRecord(channelId);
}
@action
rollbackChannelDeletion(channelId: string): void {
const channel = this.optimisticChannelBackups.get(channelId);
if (!channel) {
@@ -121,18 +119,15 @@ class Channels {
this.optimisticChannelBackups.delete(channelId);
}
@action
clearOptimisticallyRemovedChannel(channelId: string): void {
this.optimisticChannelBackups.delete(channelId);
}
@action
private removeChannel(channelId: string): void {
this.clearOptimisticallyRemovedChannel(channelId);
this.deleteChannelRecord(channelId);
}
@action
private deleteChannelRecord(channelId: string): void {
const channel = this.channelsById.get(channelId);
this.channelsById.delete(channelId);
@@ -142,7 +137,6 @@ class Channels {
ChannelDisplayName.removeChannel(channelId);
}
@action
private setChannel(channel: Channel | WireChannel): void {
const record = channel instanceof Channel ? channel : new Channel(channel);
const existing = this.channelsById.get(record.id);
@@ -154,7 +148,6 @@ class Channels {
ChannelDisplayName.syncChannel(record);
}
@action
private indexChannel(previous: Channel | undefined, next: Channel): void {
if (previous && previous.guildId === next.guildId && isPrivateChannel(previous) === isPrivateChannel(next)) {
this.replaceChannelInIndex(previous, next);
@@ -166,7 +159,6 @@ class Channels {
this.addChannelToIndex(next);
}
@action
private addChannelToIndex(channel: Channel): void {
if (channel.guildId) {
const list = this.channelsByGuildId.get(channel.guildId) ?? EMPTY_CHANNELS;
@@ -178,7 +170,6 @@ class Channels {
}
}
@action
private removeChannelFromIndex(channel: Channel): void {
if (channel.guildId) {
const list = this.channelsByGuildId.get(channel.guildId);
@@ -204,7 +195,6 @@ class Channels {
}
}
@action
private replaceChannelInIndex(previous: Channel, next: Channel): void {
if (next.guildId) {
const list = this.channelsByGuildId.get(next.guildId);
@@ -234,7 +224,6 @@ class Channels {
this.privateChannelList = updated;
}
@action
handleGatewayReady({channels}: {channels: ReadonlyArray<WireChannel>}): void {
this.channelsById.clear();
this.channelsByGuildId.clear();
@@ -270,7 +259,6 @@ class Channels {
this.setChannel(personalNotesChannel);
}
@action
handleGuildCreate(guild: GuildReadyData): void {
if (guild.unavailable) {
return;
@@ -287,7 +275,6 @@ class Channels {
}
}
@action
handleGuildDelete({guildId}: {guildId: string}): void {
const guildChannels = this.getGuildChannels(guildId);
if (guildChannels.length === 0) return;
@@ -298,12 +285,10 @@ class Channels {
}
}
@action
handleChannelCreate({channel}: {channel: WireChannel}): void {
this.setChannel(channel);
}
@action
handlePassiveLastMessageUpdates({guildId, channels}: {guildId: string; channels: Record<string, string>}): boolean {
let changed = false;
for (const [channelId, lastMessageId] of Object.entries(channels)) {
@@ -325,14 +310,12 @@ class Channels {
return changed;
}
@action
handleChannelUpdateBulk({channels}: {channels: Array<WireChannel>}): void {
for (const channel of channels) {
this.setChannel(channel);
}
}
@action
handleChannelPinsUpdate({channelId, lastPinTimestamp}: {channelId: string; lastPinTimestamp: string}): void {
const channel = this.channelsById.get(channelId);
if (!channel) {
@@ -346,7 +329,6 @@ class Channels {
);
}
@action
handleChannelRecipientAdd({channelId, user}: {channelId: string; user: UserPartial}): void {
const channel = this.channelsById.get(channelId);
if (!channel) {
@@ -361,7 +343,6 @@ class Channels {
);
}
@action
handleChannelRecipientRemove({channelId, user}: {channelId: string; user: UserPartial}): void {
const channel = this.channelsById.get(channelId);
if (!channel) {
@@ -385,7 +366,6 @@ class Channels {
);
}
@action
handleChannelDelete({channel}: {channel: WireChannel}): void {
this.removeChannel(channel.id);
const history = RouterUtils.getHistory();
@@ -408,7 +388,6 @@ class Channels {
}
}
@action
handleMessageCreate({message}: {message: WireMessage}): void {
const channel = this.channelsById.get(message.channel_id);
if (!channel) {
@@ -428,7 +407,6 @@ class Channels {
);
}
@action
handleGuildRoleDelete({guildId, roleId}: {guildId: string; roleId: string}): void {
const guildChannels = this.getGuildChannels(guildId);
if (guildChannels.length === 0) return;
@@ -8,7 +8,7 @@ export interface ChannelTopicModalProps {
export function getChannelTopicInfo(channelId: string) {
const channel = Channels.getChannel(channelId);
if (!channel || !channel.topic) return null;
if (!channel?.topic) return null;
return {
channel,
topic: channel.topic,
@@ -2,7 +2,7 @@
import {createEmojiSearchIndex} from '@app/features/emoji/state/EmojiSearchIndex';
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const EMOJI_COUNT = 20_000;
const QUERY_COUNT = 1_000;
@@ -42,21 +42,21 @@ const QUERIES = Object.freeze(
}),
);
describe('EmojiSearchIndex benchmarks', () => {
bench('builds an n-gram index for 20k emoji-like records', () => {
test('EmojiSearchIndex benchmarks', async ({bench}) => {
await bench('builds an n-gram index for 20k emoji-like records', () => {
const index = createEmojiSearchIndex(EMOJIS);
(globalThis as {__emojiSearchIndexBenchSink?: number}).__emojiSearchIndexBenchSink = index.getStats().postingCount;
});
}).run();
bench('serves 1k mixed top-10 searches over 20k records', () => {
await bench('serves 1k mixed top-10 searches over 20k records', () => {
let total = 0;
for (const query of QUERIES) {
total += INDEX.search(query, {count: 10}).length;
}
(globalThis as {__emojiSearchIndexBenchSink?: number}).__emojiSearchIndexBenchSink = total;
});
}).run();
bench('serves 1k filtered ranked searches over 20k records', () => {
await bench('serves 1k filtered ranked searches over 20k records', () => {
let total = 0;
for (const query of QUERIES) {
total += INDEX.search(query, {
@@ -66,5 +66,5 @@ describe('EmojiSearchIndex benchmarks', () => {
}).length;
}
(globalThis as {__emojiSearchIndexBenchSink?: number}).__emojiSearchIndexBenchSink = total;
});
}).run();
});
@@ -332,31 +332,31 @@ describe('ExperimentAssignments visibility', () => {
});
describe('ExperimentAssignments lifecycle', () => {
it.each([
'stop',
'reset',
] as const)('aborts an active request on %s and starts a fresh request immediately', async (method) => {
const stale = deferredReply();
ExperimentAssignments.start();
await settle();
const signal = vi.mocked(http.get).mock.calls[0]?.[1]?.signal;
expect(signal).toBeDefined();
expect(signal!.aborted).toBe(false);
it.each(['stop', 'reset'] as const)(
'aborts an active request on %s and starts a fresh request immediately',
async (method) => {
const stale = deferredReply();
ExperimentAssignments.start();
await settle();
const signal = vi.mocked(http.get).mock.calls[0]?.[1]?.signal;
expect(signal).toBeDefined();
expect(signal!.aborted).toBe(false);
ExperimentAssignments[method]();
expect(signal!.aborted).toBe(true);
vi.mocked(http.get).mockResolvedValue(reply(200, CANARY_ENVELOPE, {etag: 'W/"fresh"'}));
ExperimentAssignments.start();
await settle();
expect(http.get).toHaveBeenCalledTimes(2);
expect(ExperimentAssignments.response).toEqual(CANARY_ENVELOPE);
expect(vi.getTimerCount()).toBe(1);
ExperimentAssignments[method]();
expect(signal!.aborted).toBe(true);
vi.mocked(http.get).mockResolvedValue(reply(200, CANARY_ENVELOPE, {etag: 'W/"fresh"'}));
ExperimentAssignments.start();
await settle();
expect(http.get).toHaveBeenCalledTimes(2);
expect(ExperimentAssignments.response).toEqual(CANARY_ENVELOPE);
expect(vi.getTimerCount()).toBe(1);
stale.resolve(reply(200, INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE, {etag: 'W/"stale"'}));
await settle();
expect(ExperimentAssignments.response).toEqual(CANARY_ENVELOPE);
expect(vi.getTimerCount()).toBe(1);
});
stale.resolve(reply(200, INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE, {etag: 'W/"stale"'}));
await settle();
expect(ExperimentAssignments.response).toEqual(CANARY_ENVELOPE);
expect(vi.getTimerCount()).toBe(1);
},
);
it('keeps ownership of the replacement request when the stale request finishes first', async () => {
const stale = deferredReply();
@@ -11,7 +11,7 @@ import {
ExperimentAssignmentsResponse,
INERT_EXPERIMENT_ASSIGNMENTS_RESPONSE,
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
import {makeAutoObservable, observable, runInAction} from 'mobx';
import {makeAutoObservable, observableRef, runInAction} from 'mobx';
const logger = new Logger('ExperimentAssignments');
@@ -82,7 +82,7 @@ class ExperimentAssignmentsStore {
makeAutoObservable<this, UnobservedField>(
this,
{
response: observable.ref,
response: observableRef,
abortController: false,
etag: false,
failureStreak: false,
@@ -44,7 +44,7 @@ import LayerManager from '@app/features/ui/state/LayerManager';
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
import {DEFAULT_API_VERSION, FAVORITES_GUILD_ID} from '@fluxer/constants/src/AppConstants';
import {GatewayIdentifyFlags} from '@fluxer/constants/src/GatewayConstants';
import {action, makeAutoObservable, reaction, runInAction} from 'mobx';
import {action, actionBound, makeAutoObservable, reaction, runInAction} from 'mobx';
const logger = new Logger('GatewayConnection');
@@ -97,26 +97,26 @@ class GatewayConnection {
>(
this,
{
startSession: action.bound,
beginConnectionGrace: action.bound,
clearConnectionGrace: action.bound,
startSession: actionBound,
beginConnectionGrace: actionBound,
clearConnectionGrace: actionBound,
connectionGraceTimer: false,
previousSessionId: false,
setToken: action.bound,
sendInvisiblePresenceForCurrentSession: action.bound,
retireCurrentSession: action.bound,
logout: action.bound,
handleGatewayReady: action.bound,
handleConnectionResumed: action.bound,
handleConnectionClosed: action.bound,
cleanupSocket: action.bound,
handleGatewayDispatch: action.bound,
handleFatalGatewaySocketError: action.bound,
ensureGuildActiveAndSynced: action.bound,
flushPendingGuildSync: action.bound,
syncGuildIfNeeded: action.bound,
markGuildSynced: action.bound,
applyGatewayGeoip: action.bound,
setToken: actionBound,
sendInvisiblePresenceForCurrentSession: actionBound,
retireCurrentSession: actionBound,
logout: actionBound,
handleGatewayReady: actionBound,
handleConnectionResumed: actionBound,
handleConnectionClosed: actionBound,
cleanupSocket: actionBound,
handleGatewayDispatch: actionBound,
handleFatalGatewaySocketError: actionBound,
ensureGuildActiveAndSynced: actionBound,
flushPendingGuildSync: actionBound,
syncGuildIfNeeded: actionBound,
markGuildSynced: actionBound,
applyGatewayGeoip: actionBound,
},
{autoBind: true},
);
@@ -6,7 +6,7 @@ import Guilds from '@app/features/guild/state/Guilds';
import UserSettings, {type GuildFolder} from '@app/features/user/state/UserSettings';
import {UNCATEGORIZED_FOLDER_ID} from '@fluxer/constants/src/UserConstants';
import type {Guild as WireGuild} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
import {action, makeAutoObservable} from 'mobx';
import {makeAutoObservable} from 'mobx';
export type OrganizedItem =
| {
@@ -26,7 +26,6 @@ class GuildList {
makeAutoObservable(this, {}, {autoBind: true});
}
@action
handleGatewayReady(guilds: ReadonlyArray<GuildReadyData>): void {
const availableGuilds: Array<Guild> = [];
for (const guild of guilds) {
@@ -42,7 +41,6 @@ class GuildList {
}
}
@action
handleGuild(guild: Guild | GuildReadyData | WireGuild): void {
if (guild.unavailable) {
return;
@@ -66,7 +64,6 @@ class GuildList {
this.guilds = next;
}
@action
handleGuildDelete(guildId: string, unavailable?: boolean): void {
const index = this.guilds.findIndex((s) => s.id === guildId);
if (index === -1) {
@@ -85,7 +82,6 @@ class GuildList {
this.guilds = next;
}
@action
sortGuilds(): void {
const next = this.guilds.slice();
this.sortGuildArrayInPlace(next);
@@ -16,7 +16,7 @@ export function registerComposerCodeIndent(
): () => void {
return editor.registerCommand(
KEY_TAB_COMMAND,
(event: KeyboardEvent | null) => {
(event: KeyboardEvent) => {
if (typeaheadActiveRef.current) {
return false;
}
@@ -608,7 +608,7 @@ const ComposerInner = ({
}),
editor.registerCommand(
KEY_ARROW_UP_COMMAND,
(event: KeyboardEvent | null) => {
(event: KeyboardEvent) => {
if (typeaheadActiveState.current) {
return false;
}
@@ -662,7 +662,7 @@ const ComposerInner = ({
const target = event.target instanceof Element ? event.target : null;
const root = editor.getRootElement();
const slotHost = target == null ? null : target.closest<HTMLElement>('[data-lexical-composer-slot]');
if (slotHost != null && root != null && root.contains(slotHost)) {
if (slotHost && root?.contains(slotHost)) {
let emptySlot = false;
editor.getEditorState().read(
() => {
@@ -113,7 +113,7 @@ export function SelectionFormattingToolbarPlugin({enabled = true}: {enabled?: bo
}
const activeElement = root.ownerDocument.activeElement;
const toolbar = toolbarRef.current;
const toolbarHasFocus = activeElement != null && toolbar != null && toolbar.contains(activeElement);
const toolbarHasFocus = activeElement && toolbar?.contains(activeElement);
if (!$isRangeSelection(selection) || selection.isCollapsed()) {
if (!toolbarHasFocus || savedSelectionRef.current == null) {
setRect(null);
@@ -625,7 +625,7 @@ export function registerSlashSlotPlugin(
),
editor.registerCommand(
KEY_ARROW_RIGHT_COMMAND,
(event: KeyboardEvent | null) => {
(event: KeyboardEvent) => {
if (
event != null &&
(isIMEComposing(event) || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)
@@ -655,7 +655,7 @@ export function registerSlashSlotPlugin(
),
editor.registerCommand(
KEY_ARROW_LEFT_COMMAND,
(event: KeyboardEvent | null) => {
(event: KeyboardEvent) => {
if (
event != null &&
(isIMEComposing(event) || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)
@@ -30,7 +30,7 @@ import type {StatusType} from '@fluxer/constants/src/StatusConstants';
import {StatusTypes} from '@fluxer/constants/src/StatusConstants';
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import type {UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {makeAutoObservable, observable} from 'mobx';
import {makeAutoObservable, observableRef} from 'mobx';
interface MemberListGroup {
id: string;
@@ -258,7 +258,7 @@ class MemberSidebar {
>(
this,
{
lists: observable.ref,
lists: observableRef,
wireListChannelIds: false,
listSubscribedChannelIds: false,
syncedMemberListGuildIds: false,
@@ -9,7 +9,7 @@ import {
buildMemberListRenderWindow,
normalizeMemberListRanges,
} from '@app/features/member/utils/MemberListRangeUtils';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const ROW_HEIGHT = 44;
const VIEWPORT_HEIGHT = ROW_HEIGHT * 18;
@@ -26,12 +26,12 @@ const NORMALIZED_COVERING_RANGES = normalizeMemberListRanges(COVERING_RANGES);
const NORMALIZED_COVERING_RANGES_COPY = normalizeMemberListRanges(COVERING_RANGES_COPY);
const NORMALIZED_INNER_RANGES = normalizeMemberListRanges(INNER_RANGES);
describe('MemberListRangeUtils benchmarks', () => {
bench('normalize 1k mixed overlapping ranges', () => {
test('MemberListRangeUtils benchmarks', async ({bench}) => {
await bench('normalize 1k mixed overlapping ranges', () => {
normalizeMemberListRanges(OVERLAPPING_RANGES);
});
}).run();
bench('build subscription windows while fast scrolling', () => {
await bench('build subscription windows while fast scrolling', () => {
for (const scrollTop of SCROLL_OFFSETS) {
buildMemberListRangeWindow({
scrollTop,
@@ -42,9 +42,9 @@ describe('MemberListRangeUtils benchmarks', () => {
totalRows: LARGE_TOTAL_ROWS,
});
}
});
}).run();
bench('build render windows while fast scrolling', () => {
await bench('build render windows while fast scrolling', () => {
for (const scrollTop of SCROLL_OFFSETS) {
buildMemberListRenderWindow({
scrollTop,
@@ -54,21 +54,21 @@ describe('MemberListRangeUtils benchmarks', () => {
totalRows: LARGE_TOTAL_ROWS,
});
}
});
}).run();
bench('coverage checks for 1k normalized pages', () => {
await bench('coverage checks for 1k normalized pages', () => {
areMemberListRangesCovered(INNER_RANGES, COVERING_RANGES);
});
}).run();
bench('normalized coverage checks without re-normalizing', () => {
await bench('normalized coverage checks without re-normalizing', () => {
areNormalizedMemberListRangesCovered(NORMALIZED_INNER_RANGES, NORMALIZED_COVERING_RANGES);
});
}).run();
bench('range equality for raw normalized-shaped windows', () => {
await bench('range equality for raw normalized-shaped windows', () => {
areMemberListRangesEqual(COVERING_RANGES, COVERING_RANGES_COPY);
});
}).run();
bench('normalized range equality without re-normalizing', () => {
await bench('normalized range equality without re-normalizing', () => {
areNormalizedMemberListRangesEqual(NORMALIZED_COVERING_RANGES, NORMALIZED_COVERING_RANGES_COPY);
});
}).run();
});
@@ -4,7 +4,7 @@ import type {MarkdownParseOptions} from '@app/features/messaging/components/mark
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
import {parseMarkdownContent as parse} from '@app/features/messaging/utils/markdown/MarkdownParseCache';
import type {Node} from '@app/features/messaging/utils/markdown/parser/Nodes';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const SIMPLE_CONTENT = 'hello **world** with <@1234567890> and https://fluxer.app';
const RICH_CONTENT = [
@@ -30,25 +30,25 @@ const SIMPLE_AST = parse({content: SIMPLE_CONTENT, context: MarkdownContext.STAN
const RICH_AST = parse({content: RICH_CONTENT, context: MarkdownContext.STANDARD_WITHOUT_JUMBO}).nodes;
let missSerial = 0;
describe('Markdown rendering benchmarks', () => {
bench('parse cache hit for simple message content', () => {
test('Markdown rendering benchmarks', async ({bench}) => {
await bench('parse cache hit for simple message content', () => {
parse({content: SIMPLE_CONTENT, context: MarkdownContext.STANDARD_WITHOUT_JUMBO});
});
}).run();
bench('parse cache miss for rich message content', () => {
await bench('parse cache miss for rich message content', () => {
missSerial += 1;
parse({content: `${RICH_CONTENT}\n${missSerial}`, context: MarkdownContext.STANDARD_WITHOUT_JUMBO});
});
}).run();
bench('render simple parsed markdown AST to React nodes', () => {
await bench('render simple parsed markdown AST to React nodes', () => {
let count = 0;
for (const node of SIMPLE_AST) {
count += node.type.length;
}
(globalThis as {__markdownRenderBenchSink?: number}).__markdownRenderBenchSink = count + RENDER_OPTIONS.context;
});
}).run();
bench('walk rich parsed markdown AST for render preparation', () => {
await bench('walk rich parsed markdown AST for render preparation', () => {
let count = 0;
const stack = [...RICH_AST];
while (stack.length > 0) {
@@ -62,5 +62,5 @@ describe('Markdown rendering benchmarks', () => {
}
}
(globalThis as {__markdownRenderBenchSink?: number}).__markdownRenderBenchSink = count + RENDER_OPTIONS.context;
});
}).run();
});
@@ -441,7 +441,7 @@ export function useTextareaAutocomplete({
);
const canManageUser = useCallback(
(otherUserId: string, permission: bigint) => {
if (!channel || !channel.guildId) return false;
if (!channel?.guildId) return false;
const currentUserId = Authentication.currentUserId;
if (otherUserId === currentUserId) return false;
const guild = Guilds.getGuild(channel.guildId);
@@ -452,7 +452,7 @@ export function useTextareaAutocomplete({
);
const canViewChannel = useCallback(
(userId: string): boolean => {
if (!channel || !channel.guildId) {
if (!channel?.guildId) {
return true;
}
return PermissionUtils.can(Permissions.VIEW_CHANNEL, userId as UserId, channel.toJSON());
@@ -491,7 +491,7 @@ export function useTextareaAutocomplete({
}
case 'mention': {
setAutocompleteType('mention');
if (!channel || !channel.guildId) {
if (!channel?.guildId) {
if (!channel) {
setAutocompleteOptions([]);
return;
@@ -2,7 +2,7 @@
import TextareaSelection from '@app/features/messaging/state/TextareaSelection';
import AppStorage from '@app/features/platform/state/PersistentStorage';
import {comparer, makeAutoObservable, reaction} from 'mobx';
import {compareStructural, makeAutoObservable, reaction} from 'mobx';
const MESSAGE_EDIT_STORAGE_KEY = 'MessageEdit';
@@ -83,7 +83,7 @@ class MessageEdit {
return reaction(
() => Object.entries(this.editingMessageIds),
() => callback(),
{fireImmediately: true, equals: comparer.structural},
{fireImmediately: true, equals: compareStructural},
);
}
}
@@ -602,7 +602,7 @@ export class MessageQueue extends Queue<MessageQueuePayload, RestResponse<Messag
private scheduleTextareaAttachmentUploadCleanup(attachmentId: number): void {
window.setTimeout(() => {
const entry = this.textareaAttachmentUploads.get(attachmentId);
if (!entry || !entry.settled) {
if (!entry?.settled) {
return;
}
if (Date.now() - entry.startedAt >= TEXTAREA_ATTACHMENT_UPLOAD_CACHE_TTL_MS) {
@@ -980,7 +980,7 @@ export class MessageQueue extends Queue<MessageQueuePayload, RestResponse<Messag
});
const plans = response.body?.attachments ?? [];
for (const entry of plans) {
if (!entry || !entry.upload_mode || !entry.upload_filename || !entry.filename) {
if (!entry?.upload_mode || !entry.upload_filename || !entry.filename) {
throw new Error('Invalid presigned attachment upload response');
}
if (entry.upload_mode === 'singlepart') {
@@ -3,7 +3,7 @@
import TextareaSelection from '@app/features/messaging/state/TextareaSelection';
import type {MentionSegment} from '@app/features/messaging/utils/TextareaSegmentManager';
import {makePersistent} from '@app/features/platform/utils/MobXPersistence';
import {action, makeAutoObservable} from 'mobx';
import {makeAutoObservable} from 'mobx';
const EMPTY_SEGMENTS: ReadonlyArray<MentionSegment> = [];
@@ -61,7 +61,6 @@ class Drafts {
await makePersistent(this, 'Drafts', ['drafts', 'draftSegments']);
}
@action
createDraft(channelId: string, content: string, segments?: ReadonlyArray<MentionSegment> | null): void {
if (!content) {
this.deleteDraft(channelId);
@@ -80,7 +79,6 @@ class Drafts {
}
}
@action
deleteDraft(channelId: string): void {
if (!(channelId in this.drafts) && !(channelId in this.draftSegments)) {
return;
@@ -90,7 +88,6 @@ class Drafts {
TextareaSelection.clearChannelSelection(channelId);
}
@action
deleteChannelDraft(channelId: string): void {
this.deleteDraft(channelId);
}
@@ -103,7 +100,6 @@ class Drafts {
return this.draftSegments[channelId] ?? EMPTY_SEGMENTS;
}
@action
cleanupEmptyDrafts(): void {
for (const channelId of Object.keys(this.drafts)) {
const content = this.drafts[channelId];
@@ -21,7 +21,7 @@ import {MAX_MESSAGES_PER_CHANNEL} from '@fluxer/constants/src/LimitConstants';
import type {ChannelId} from '@fluxer/schema/src/branded/WireIds';
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import type {Message as WireMessage} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import {action, makeAutoObservable, reaction} from 'mobx';
import {makeAutoObservable, reaction} from 'mobx';
const STALE_WINDOW_REFETCH_INTERVAL_MS = 10_000;
const staleWindowRefetchedAt = new Map<string, number>();
@@ -69,7 +69,6 @@ class Messages {
return this.updateCounter;
}
@action
private notifyChange(): void {
this.updateCounter += 1;
}
@@ -217,7 +216,6 @@ class Messages {
return messages.length === 0 ? !messages.ready : messages.cached;
}
@action
preloadLatestPage(channelId: string, guildId?: string | null): boolean {
if (!this.shouldPreloadLatestPage(channelId)) {
return false;
@@ -248,7 +246,6 @@ class Messages {
return channel?.hasNewestMessages() ?? false;
}
@action
setPendingJumpDispatch(channelId: string, dispatch: PendingJumpDispatch): void {
this.pendingJumpDispatches.set(channelId, dispatch);
}
@@ -260,7 +257,6 @@ class Messages {
return dispatch.messageId === messageId ? dispatch : null;
}
@action
handleConnectionClosed(): boolean {
let didUpdate = false;
ChannelMessages.forEach((messages) => {
@@ -275,7 +271,6 @@ class Messages {
return false;
}
@action
handleSessionInvalidated(): boolean {
const channelIds: Array<string> = [];
ChannelMessages.forEach((messages) => channelIds.push(messages.channelId));
@@ -291,7 +286,6 @@ class Messages {
return true;
}
@action
handleGatewayReady(): boolean {
const selectedChannelId = SelectedChannel.currentChannelId;
let didHydrateSelectedChannel = false;
@@ -340,7 +334,6 @@ class Messages {
MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL);
}
@action
handleChannelSelect(action: {guildId?: string; channelId?: string | null; messageId?: string}): boolean {
const channelId = action.channelId ?? action.guildId;
if (channelId == null || channelId === ME) {
@@ -388,7 +381,6 @@ class Messages {
return false;
}
@action
handleGuildUnavailable(guildId: string, unavailable: boolean): boolean {
if (!unavailable) {
return false;
@@ -416,7 +408,6 @@ class Messages {
return didUpdate;
}
@action
handleGuildCreate(action: {
guild: {
id: string;
@@ -444,7 +435,6 @@ class Messages {
return didChannelSelect;
}
@action
handleLoadMessages(action: {channelId: string; jump?: JumpOptions}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId);
this.commitMessages(messages.beginLoad(action.jump));
@@ -452,7 +442,6 @@ class Messages {
return false;
}
@action
handleTruncateMessages(action: {channelId: string; trimNewest?: boolean; trimOldest?: boolean}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId).trimToWindow(
action.trimNewest ?? false,
@@ -463,7 +452,6 @@ class Messages {
return false;
}
@action
handleLoadMessagesSuccessCached(action: {
channelId: string;
jump?: JumpOptions;
@@ -492,7 +480,6 @@ class Messages {
return false;
}
@action
handleLoadMessagesSuccess(action: {
channelId: string;
isBefore?: boolean;
@@ -531,7 +518,6 @@ class Messages {
MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL, jump, {staleRefetch: true});
}
@action
handleLoadMessagesFailure(action: {channelId: string}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId);
this.commitMessages(messages.withPatch({loadingMore: false, error: true}));
@@ -539,7 +525,6 @@ class Messages {
return false;
}
@action
handleLoadMessagesBlocked(action: {channelId: string}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId);
if (!messages.loadingMore && !messages.error) {
@@ -550,7 +535,6 @@ class Messages {
return true;
}
@action
handleIncomingMessage(action: {channelId: string; message: WireMessage}): boolean {
Channels.handleMessageCreate({message: action.message});
const existing = ChannelMessages.get(action.channelId);
@@ -563,30 +547,27 @@ class Messages {
return false;
}
@action
handleSendFailed(action: {channelId: string; nonce: string}): boolean {
const existing = ChannelMessages.get(action.channelId);
if (!existing || !existing.has(action.nonce)) return false;
if (!existing?.has(action.nonce)) return false;
const updated = existing.update(action.nonce, (message) => message.withUpdates({state: MessageStates.FAILED}));
this.commitMessages(updated);
this.notifyChange();
return true;
}
@action
handleSendRetry(action: {channelId: string; messageId: string}): boolean {
const existing = ChannelMessages.get(action.channelId);
if (!existing || !existing.has(action.messageId)) return false;
if (!existing?.has(action.messageId)) return false;
const updated = existing.update(action.messageId, (message) => message.withUpdates({state: MessageStates.SENDING}));
this.commitMessages(updated);
this.notifyChange();
return true;
}
@action
handleMessageDelete(action: {id: string; channelId: string}): boolean {
const existing = ChannelMessages.get(action.channelId);
if (!existing || !existing.has(action.id)) {
if (!existing?.has(action.id)) {
return false;
}
let messages = existing;
@@ -600,7 +581,6 @@ class Messages {
return true;
}
@action
handleMessageDeleteBulk(action: {ids: Array<string>; channelId: string}): boolean {
const existing = ChannelMessages.get(action.channelId);
if (!existing) return false;
@@ -615,12 +595,11 @@ class Messages {
return true;
}
@action
handleMessageUpdate(action: {message: WireMessage}): boolean {
const messageId = action.message.id;
const channelId = action.message.channel_id;
const existing = ChannelMessages.get(channelId);
if (!existing || !existing.has(messageId)) return false;
if (!existing?.has(messageId)) return false;
const updated = existing.update(messageId, (message) => {
if (message.isEditing && action.message.state === undefined) {
return message.withUpdates({...action.message, state: MessageStates.SENT});
@@ -632,7 +611,6 @@ class Messages {
return true;
}
@action
handleUserUpdate(action: {
user: {
id: string;
@@ -649,7 +627,6 @@ class Messages {
return hasChanges;
}
@action
handleGuildMemberUpdate(action: GuildMemberUpdateAction): boolean {
const userId = action.member.user.id;
const updatedAuthor = Users.getUser(userId);
@@ -664,7 +641,6 @@ class Messages {
return hasChanges;
}
@action
handlePresenceUpdate(action: PresenceUpdateAction): boolean {
if (!action.presence.user.username && !action.presence.user.avatar && !action.presence.user.discriminator) {
return false;
@@ -684,7 +660,6 @@ class Messages {
return hasChanges;
}
@action
handleCleanup(): boolean {
ChannelMessages.forEach(({channelId}) => {
if (Channels.getChannel(channelId) == null) {
@@ -696,7 +671,6 @@ class Messages {
return false;
}
@action
handleRelationshipUpdate(): boolean {
let hasChanges = false;
ChannelMessages.forEach((messages) => {
@@ -713,7 +687,6 @@ class Messages {
return false;
}
@action
handleMessageReveal(action: {channelId: string; messageId: string | null}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId);
this.commitMessages(messages.withPatch({unblurredMessageId: action.messageId}));
@@ -721,7 +694,6 @@ class Messages {
return true;
}
@action
handleClearJumpTarget(action: {channelId: string; clearReturnTarget?: boolean}): boolean {
const messages = ChannelMessages.get(action.channelId);
if (
@@ -737,7 +709,6 @@ class Messages {
return false;
}
@action
handleReaction(action: {
type: 'MESSAGE_REACTION_ADD' | 'MESSAGE_REACTION_REMOVE';
channelId: string;
@@ -765,7 +736,6 @@ class Messages {
return true;
}
@action
handleRemoveAllReactions(action: {channelId: string; messageId: string}): boolean {
const existing = ChannelMessages.get(action.channelId);
if (!existing) return false;
@@ -775,7 +745,6 @@ class Messages {
return true;
}
@action
handleRemoveReactionEmoji(action: {channelId: string; messageId: string; emoji: ReactionEmoji}): boolean {
const existing = ChannelMessages.get(action.channelId);
if (!existing) return false;
@@ -785,7 +754,6 @@ class Messages {
return true;
}
@action
handleMessagePreload(action: {messages: Record<ChannelId, WireMessage>}): boolean {
let hasChanges = false;
for (const [channelId, messageData] of Object.entries(action.messages)) {
@@ -803,7 +771,6 @@ class Messages {
return hasChanges;
}
@action
handleOptimisticEdit(action: {channelId: string; messageId: string; content: string}): {
originalContent: string;
originalEditedTimestamp: string | null;
@@ -828,7 +795,6 @@ class Messages {
return rollbackData;
}
@action
handleEditRollback(action: {
channelId: string;
messageId: string;
@@ -837,7 +803,7 @@ class Messages {
}): void {
const {channelId, messageId, originalContent, originalEditedTimestamp} = action;
const existing = ChannelMessages.get(channelId);
if (!existing || !existing.has(messageId)) return;
if (!existing?.has(messageId)) return;
const updated = existing.update(messageId, (msg) =>
msg.withUpdates({
content: originalContent,
@@ -7,15 +7,15 @@ import {
transitionReactionSnapshot,
} from '@app/features/messaging/state/ReactionStateMachine';
import type {ReactionEmoji} from '@app/features/messaging/utils/ReactionUtils';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const EMOJIS: Array<ReactionEmoji> = Array.from({length: 32}, (_value, index) => ({
id: index % 3 === 0 ? `emoji-${index}` : undefined,
name: index % 3 === 0 ? `custom_${index}` : ['🔥', '❤️', '👍', '🎉'][index % 4],
}));
describe('ReactionStateMachine benchmarks', () => {
bench('apply 1k reaction add/remove transitions for visible messages', () => {
test('ReactionStateMachine benchmarks', async ({bench}) => {
await bench('apply 1k reaction add/remove transitions for visible messages', () => {
let snapshot = createReactionMachineSnapshot(emptyMap(), 'me');
for (let index = 0; index < 1_000; index += 1) {
const emoji = EMOJIS[index % EMOJIS.length];
@@ -36,9 +36,9 @@ describe('ReactionStateMachine benchmarks', () => {
}
}
mapToReactions(snapshot.context.map);
});
}).run();
bench('hydrate 500-message reaction payload shape', () => {
await bench('hydrate 500-message reaction payload shape', () => {
let snapshot = createReactionMachineSnapshot(emptyMap(), 'me');
for (let index = 0; index < 500; index += 1) {
snapshot = transitionReactionSnapshot(snapshot, {
@@ -52,5 +52,5 @@ describe('ReactionStateMachine benchmarks', () => {
});
}
mapToReactions(snapshot.context.map);
});
}).run();
});
@@ -4,7 +4,7 @@ import {
calculateAroundPaginationState,
getAroundWindowCounts,
} from '@app/features/messaging/utils/MessagePaginationUtils';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const PAGINATION_INPUTS = Array.from({length: 100_000}, (_value, index) => ({
limit: 50 + (index % 25),
@@ -14,19 +14,19 @@ const PAGINATION_INPUTS = Array.from({length: 100_000}, (_value, index) => ({
knownLatestMessageId: 'latest',
}));
describe('MessagePaginationUtils benchmarks', () => {
bench('calculate 100k around pagination states', () => {
test('MessagePaginationUtils benchmarks', async ({bench}) => {
await bench('calculate 100k around pagination states', () => {
for (const input of PAGINATION_INPUTS) {
calculateAroundPaginationState(input);
}
});
}).run();
bench('calculate 100k around window counts', () => {
await bench('calculate 100k around window counts', () => {
let total = 0;
for (let index = 0; index < 100_000; index += 1) {
const {newer, older} = getAroundWindowCounts(index % 100);
total += newer + older;
}
(globalThis as {__messagePaginationBenchSink?: number}).__messagePaginationBenchSink = total;
});
}).run();
});
@@ -5,7 +5,7 @@ import * as RouterUtils from '@app/features/navigation/utils/RouterUtils';
import type {Router} from '@app/features/platform/components/router/RouterTypes';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {ME} from '@fluxer/constants/src/AppConstants';
import {action, makeAutoObservable} from 'mobx';
import {makeAutoObservable} from 'mobx';
type NavigationMode = 'push' | 'replace';
@@ -23,7 +23,6 @@ class Navigation {
makeAutoObservable(this, {}, {autoBind: true});
}
@action
initialize(router: Router): void {
this.router = router;
this.updateFromRouter();
@@ -32,7 +31,6 @@ class Navigation {
});
}
@action
private updateFromRouter(): void {
if (!this.router) return;
const state = this.router.getState();
@@ -7,7 +7,7 @@ import {makePersistent} from '@app/features/platform/utils/MobXPersistence';
import {FAVORITES_GUILD_ID, ME} from '@fluxer/constants/src/AppConstants';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import type {Channel} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import {action, computed, makeAutoObservable, reaction} from 'mobx';
import {computed, makeAutoObservable, reaction} from 'mobx';
interface ChannelVisit {
channelId: string;
@@ -44,7 +44,6 @@ class SelectedChannel {
void this.initPersistence();
}
@action
private async initPersistence(): Promise<void> {
await makePersistent(this, 'SelectedChannel', ['selectedChannelIds', 'recentlyVisitedChannels']);
this.migrateRecentVisits();
@@ -89,7 +88,6 @@ class SelectedChannel {
return this.selectedChannelIds.get(guildId) ?? null;
}
@action
private migrateRecentVisits(): void {
let needsMigration = false;
for (let i = 0; i < this.recentlyVisitedChannels.length; i++) {
@@ -126,7 +124,6 @@ class SelectedChannel {
return result;
}
@action
selectChannel(guildId?: string, channelId?: string | null): void {
const normalizedGuildId = this.normalizeGuildId(guildId ?? null);
if (!normalizedGuildId) return;
@@ -186,7 +183,6 @@ class SelectedChannel {
this.viewedChannelHistoryIndex = this.viewedChannelHistory.length - 1;
}
@action
navigateViewedChannelHistory(direction: -1 | 1): boolean {
let nextIndex = this.viewedChannelHistoryIndex + direction;
let target = this.viewedChannelHistory[nextIndex];
@@ -213,7 +209,6 @@ class SelectedChannel {
return true;
}
@action
deselectChannel(): void {
const guildId = this.getCurrentGuildId();
if (guildId != null) {
@@ -221,14 +216,12 @@ class SelectedChannel {
}
}
@action
clearGuildSelection(guildId: string): void {
const normalizedGuildId = this.normalizeGuildId(guildId);
if (!normalizedGuildId) return;
this.removeGuildSelection(normalizedGuildId);
}
@action
getNavigableSelectedChannelId(guildId: string): string | null {
const normalizedGuildId = this.normalizeGuildId(guildId);
if (!normalizedGuildId) return null;
@@ -241,7 +234,6 @@ class SelectedChannel {
return null;
}
@action
handleChannelDelete(channel: Channel): void {
const guildId = channel.guild_id ?? ME;
const normalizedGuildId = this.normalizeGuildId(guildId) ?? guildId;
@@ -251,12 +243,10 @@ class SelectedChannel {
}
}
@action
private removeGuildSelection(guildId: string): void {
this.selectedChannelIds.delete(guildId);
}
@action
getValidatedFavoritesChannel(): string | null {
const selectedChannelId = this.getNavigableSelectedChannelId(FAVORITES_GUILD_ID);
if (selectedChannelId && Favorites.isChannelAccessible(selectedChannelId)) {
@@ -3,7 +3,7 @@
import Navigation from '@app/features/navigation/state/Navigation';
import {makePersistent} from '@app/features/platform/utils/MobXPersistence';
import {ME} from '@fluxer/constants/src/AppConstants';
import {action, makeAutoObservable, reaction} from 'mobx';
import {makeAutoObservable, reaction} from 'mobx';
const FAVORITES_ROUTE_ID = '@favorites';
@@ -18,7 +18,6 @@ class SelectedGuild {
void this.initPersistence();
}
@action
private async initPersistence(): Promise<void> {
await makePersistent(this, 'SelectedGuild', ['lastSelectedGuildId']);
this.setupNavigationReaction();
@@ -49,7 +48,6 @@ class SelectedGuild {
return guildId;
}
@action
selectGuild(guildId: string, _forceSync = false): void {
if (!guildId) {
return;
@@ -57,12 +55,10 @@ class SelectedGuild {
this.setGuild(guildId, {forceNonce: true});
}
@action
syncCurrentGuild(): void {
this.bumpNonce();
}
@action
deselectGuild(): void {
this.clearSelection();
}
@@ -333,7 +333,7 @@ export function parseChannelJumpLink(url: string): ChannelJumpLink | null {
export function parseMessageJumpLink(url: string): MessageJumpLink | null {
const segments = getChannelSegments(url);
if (!segments || segments.length !== 4) return null;
if (segments?.length !== 4) return null;
const [, scope, channelId, messageId] = segments;
if (!messageId || !isProbablyAValidSnowflake(messageId)) {
return null;
@@ -18,7 +18,7 @@ import CompactVoiceCallHeight, {getGuildVoiceCallExpansionKey} from '@app/featur
import VoiceCallFullscreen from '@app/features/voice/state/VoiceCallFullscreen';
import Window from '@app/features/window/state/Window';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import {action, makeAutoObservable, reaction} from 'mobx';
import {makeAutoObservable, reaction} from 'mobx';
const logger = new Logger('AutoAck');
@@ -77,7 +77,6 @@ class AutoAck {
});
}
@action
private updateAutoAckState(conditions: {
windowId: string;
channelId: string | null;
@@ -103,7 +102,6 @@ class AutoAck {
}
}
@action
private enableAutomaticAckInternal(channelId: string, windowId: string): void {
const channel = Channels.getChannel(channelId);
if (channel == null) {
@@ -121,7 +119,6 @@ class AutoAck {
}
}
@action
private disableAutomaticAckInternal(channelId: string, windowId: string): void {
const channels = this.windowChannels.get(windowId);
if (channels == null) return;
@@ -141,7 +138,6 @@ class AutoAck {
return false;
}
@action
disableForChannel(channelId: string): void {
for (const [windowId, channels] of this.windowChannels.entries()) {
if (channels.has(channelId)) {
@@ -30,7 +30,7 @@ import AppStorage from '@app/features/platform/state/PersistentStorage';
import {http} from '@app/features/platform/transport/RestTransport';
import {Logger} from '@app/features/platform/utils/AppLogger';
import LocalPresence from '@app/features/presence/state/LocalPresence';
import {action, makeAutoObservable} from 'mobx';
import {actionBound, makeAutoObservable} from 'mobx';
export {type Account, SessionState};
@@ -149,10 +149,10 @@ export class AuthSessionManager {
makeAutoObservable(
this,
{
send: action.bound,
setToken: action.bound,
setUserId: action.bound,
setError: action.bound,
send: actionBound,
setToken: actionBound,
setUserId: actionBound,
setError: actionBound,
},
{autoBind: true},
);
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {evaluateScrollPinning} from '@app/features/platform/utils/ScrollPosition';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const SCROLL_METRICS = Array.from({length: 100_000}, (_value, index) => ({
scrollTop: (index * 37) % 200_000,
@@ -9,8 +9,8 @@ const SCROLL_METRICS = Array.from({length: 100_000}, (_value, index) => ({
offsetHeight: 800 + (index % 200),
}));
describe('ScrollPosition benchmarks', () => {
bench('evaluate 100k message-list scroll pinning states', () => {
test('ScrollPosition benchmarks', async ({bench}) => {
await bench('evaluate 100k message-list scroll pinning states', () => {
let pinned = 0;
for (let index = 0; index < SCROLL_METRICS.length; index += 1) {
if (
@@ -23,5 +23,5 @@ describe('ScrollPosition benchmarks', () => {
}
}
(globalThis as {__scrollPositionBenchSink?: number}).__scrollPositionBenchSink = pinned;
});
}).run();
});
@@ -21,7 +21,7 @@ import type {StatusType} from '@fluxer/constants/src/StatusConstants';
import {normalizeStatus, StatusTypes} from '@fluxer/constants/src/StatusConstants';
import {RelationshipTypes} from '@fluxer/constants/src/UserConstants';
import type {UserPrivate} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {makeAutoObservable, observable, reaction} from 'mobx';
import {makeAutoObservable, observable, observableShallow, reaction} from 'mobx';
interface FlattenedPresence {
status: StatusType;
@@ -61,7 +61,7 @@ class Presence {
presences: false,
remotePresenceCountsByGuild: false,
remotePresenceCountVersionByGuild: false,
mobilePresenceUserIds: observable.shallow,
mobilePresenceUserIds: observableShallow,
suppressVersionBump: false,
},
{autoBind: true},
@@ -5,7 +5,7 @@ import {
resolveReadStateEntryStatus,
} from '@app/features/read_state/state/read_states/ReadStateEntryStatusMachine';
import {fromTimestamp} from '@fluxer/snowflake/src/SnowflakeUtils';
import {bench, describe} from 'vitest';
import {test} from 'vitest';
const BASE_TIMESTAMP = Date.UTC(2024, 0, 1);
const ACK_ID = fromTimestamp(BASE_TIMESTAMP + 1_000);
@@ -79,8 +79,8 @@ const INPUTS = Object.freeze(
}),
);
describe('ReadStateEntryStatusMachine benchmarks', () => {
bench('resolves 100k mixed read-state entry statuses', () => {
test('ReadStateEntryStatusMachine benchmarks', async ({bench}) => {
await bench('resolves 100k mixed read-state entry statuses', () => {
let unreadOrMentionCount = 0;
for (const input of INPUTS) {
if (resolveReadStateEntryStatus(input).isUnreadOrMentioned) {
@@ -88,5 +88,5 @@ describe('ReadStateEntryStatusMachine benchmarks', () => {
}
}
(globalThis as {__readStateEntryStatusBenchSink?: number}).__readStateEntryStatusBenchSink = unreadOrMentionCount;
});
}).run();
});
@@ -34,7 +34,7 @@ import MobileLayout from '@app/features/ui/state/MobileLayout';
import {QuickSwitcherResultTypes} from '@fluxer/constants/src/QuickSwitcherConstants';
import type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import {action, makeAutoObservable, runInAction} from 'mobx';
import {makeAutoObservable, runInAction} from 'mobx';
const GO_TO_MESSAGE_DESCRIPTOR = msg({
message: 'Go to message',
@@ -157,7 +157,6 @@ class QuickSwitcher {
return this.selectedIndex;
}
@action
show(): void {
if (this.isOpen) return;
this.cancelCandidateWarmup();
@@ -215,7 +214,6 @@ class QuickSwitcher {
);
}
@action
hide(): void {
if (!this.isOpen) {
return;
@@ -291,7 +289,6 @@ class QuickSwitcher {
return () => clearTimeout(handle);
}
@action
search(query: string): void {
if (!this.isOpen && query.length === 0) {
return;
@@ -372,7 +369,6 @@ class QuickSwitcher {
}, 300);
}
@action
select(selectedIndex: number): void {
if (!this.isOpen) {
return;
@@ -2,7 +2,7 @@
import {makePersistent} from '@app/features/platform/utils/MobXPersistence';
import type {SearchHints} from '@app/features/search/utils/SearchQueryParser';
import {action, makeAutoObservable} from 'mobx';
import {makeAutoObservable} from 'mobx';
export const SEARCH_HISTORY_LIMIT = 10;
export const SEARCH_HISTORY_DISPLAY_LIMIT = 5;
@@ -37,7 +37,6 @@ class SearchHistory {
return entries.filter((e) => e.query.toLowerCase().includes(t));
}
@action
add(query: string, channelId?: string, hints?: SearchHints): void {
if (!channelId) return;
const q = query.trim();
@@ -58,7 +57,6 @@ class SearchHistory {
}
}
@action
adoptLegacyEntries(targetKey: string | undefined, legacyKeys: ReadonlyArray<string>): void {
if (!targetKey) return;
if (this.getEntries(targetKey).length > 0) return;
@@ -83,13 +81,11 @@ class SearchHistory {
this.entriesByChannel[targetKey] = adopted;
}
@action
clear(channelId?: string): void {
if (!channelId) return;
delete this.entriesByChannel[channelId];
}
@action
clearAll(): void {
this.entriesByChannel = {};
}
+1 -1
View File
@@ -155,7 +155,7 @@ class Theme {
private initLinuxPortalThemeDetection(): void {
const electronApi = getElectronAPI();
if (!electronApi || electronApi.platform !== 'linux') {
if (electronApi?.platform !== 'linux') {
return;
}
if (!electronApi.getLinuxAppearance || !electronApi.onLinuxAppearanceChanged) {
@@ -348,7 +348,7 @@ export const UserContextMenu: React.FC<UserContextMenuProps> = observer(
);
}, [channel, onClose, user.id, userDisplayName, i18n]);
const handleCloseDM = useCallback(() => {
if (!channel || !channel.isDM()) return;
if (!channel?.isDM()) return;
onClose();
const displayName = dmPartner ? NicknameUtils.getNickname(dmPartner) : NicknameUtils.getNickname(user);
ModalCommands.push(
@@ -172,7 +172,7 @@ export const useTextInputContextMenu = () => {
useEffect(() => {
if (!nativeShim) return;
const electronAPI = getElectronAPI();
if (!electronAPI || !electronAPI.onTextareaContextMenu) return;
if (!electronAPI?.onTextareaContextMenu) return;
let recentTarget: RecentContextMenuTarget | null = null;
const onContextMenuCapture = (event: MouseEvent) => {
const editable = getEditableTarget(event.target as Element | null);
@@ -8,7 +8,7 @@ export function getPopoutFocusManagerInsideElements(
): Array<Element> {
const insideElements: Array<Element> = [];
const addElement = (element: Element | null | undefined): void => {
if (!element || !element.isConnected || insideElements.includes(element)) {
if (!element?.isConnected || insideElements.includes(element)) {
return;
}
insideElements.push(element);
@@ -3,7 +3,7 @@
import {Logger} from '@app/features/platform/utils/AppLogger';
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
import {shouldRestoreFocusToTarget} from '@app/features/ui/utils/PointerActivationFocus';
import {makeAutoObservable, observable} from 'mobx';
import {makeAutoObservable, observableRef} from 'mobx';
import type React from 'react';
const logger = new Logger('ContextMenu');
@@ -74,7 +74,7 @@ class ContextMenuState {
private currentEntry: StackEntry | null = null;
constructor() {
makeAutoObservable<this, 'currentEntry'>(this, {currentEntry: observable.ref}, {autoBind: true});
makeAutoObservable<this, 'currentEntry'>(this, {currentEntry: observableRef}, {autoBind: true});
}
get contextMenu(): ContextMenu | null {
@@ -2,7 +2,7 @@
import type {Channel} from '@app/features/channel/models/Channel';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {makeAutoObservable, observable} from 'mobx';
import {makeAutoObservable, observableRef} from 'mobx';
export type MediaViewerItem = Readonly<{
src: string;
@@ -42,9 +42,9 @@ class MediaViewer {
makeAutoObservable(
this,
{
items: observable.ref,
message: observable.ref,
sourceChannel: observable.ref,
items: observableRef,
message: observableRef,
sourceChannel: observableRef,
},
{autoBind: true},
);
+1 -1
View File
@@ -131,7 +131,7 @@ class PiP {
}
}
export {PIP_DEFAULT_WIDTH};
export type {PiPContent, PiPContentType, PiPCorner};
export {PIP_DEFAULT_WIDTH};
export default new PiP();
+1 -1
View File
@@ -310,7 +310,7 @@ class Popout {
logger.debug(
`Popout.scheduleFocus target=${meta?.target ? meta.target.tagName : 'null'} keyboardMode=${meta?.keyboardModeEnabled ?? false}`,
);
if (!meta || !meta.restoreFocus || !meta.target) return;
if (!meta?.restoreFocus || !meta.target) return;
const {target, keyboardModeEnabled} = meta;
queueMicrotask(() => {
const hasHiddenAncestor = (element: HTMLElement): boolean =>
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {makeAutoObservable, observable} from 'mobx';
import {makeAutoObservable, observableRef} from 'mobx';
import type React from 'react';
class SettingsSidebar {
@@ -10,7 +10,7 @@ class SettingsSidebar {
dismissedOwnerId: string | null = null;
constructor() {
makeAutoObservable(this, {overrideContent: observable.ref}, {autoBind: true});
makeAutoObservable(this, {overrideContent: observableRef}, {autoBind: true});
}
get hasOverride(): boolean {
+2 -2
View File
@@ -2,7 +2,7 @@
import {Logger} from '@app/features/platform/utils/AppLogger';
import type {ToastProps} from '@app/features/ui/toast';
import {makeAutoObservable, observable} from 'mobx';
import {makeAutoObservable, observableRef} from 'mobx';
const logger = new Logger('Toast');
@@ -18,7 +18,7 @@ class Toast {
makeAutoObservable(
this,
{
currentToast: observable.ref,
currentToast: observableRef,
},
{autoBind: true},
);
@@ -643,7 +643,7 @@ export const Tooltip = observer(
useEffect(() => {
if (!openOnMountHover || !hasChild || mobileLayout.enabled || isDisabled) return;
const target = targetRef.current;
if (!target || !target.matches(':hover')) return;
if (!target?.matches(':hover')) return;
beginVisibilityDriver('hover');
}, [beginVisibilityDriver, hasChild, isDisabled, mobileLayout.enabled, openOnMountHover]);
useEffect(() => {
@@ -679,7 +679,7 @@ export const Tooltip = observer(
let frameId: number | null = null;
const verifyTargetHover = () => {
const target = targetRef.current;
if (!target || !target.matches(':hover')) {
if (!target?.matches(':hover')) {
endVisibilityDriver('hover');
frameId = null;
return;
@@ -109,7 +109,7 @@ export async function getNativePlatform(): Promise<NativePlatform> {
export async function isLinuxWaylandDesktopSession(): Promise<boolean> {
const electronApi = getElectronAPI();
if (!electronApi || electronApi.platform !== 'linux') {
if (electronApi?.platform !== 'linux') {
return false;
}
try {
@@ -5,7 +5,7 @@ import type {DeveloperApplicationWire} from '@app/features/devtools/models/Devel
import {DeveloperApplication} from '@app/features/devtools/models/DeveloperApplication';
import {http} from '@app/features/platform/transport/RestTransport';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {action, makeAutoObservable, runInAction} from 'mobx';
import {makeAutoObservable, runInAction} from 'mobx';
const logger = new Logger('ApplicationsTabState');
@@ -196,7 +196,6 @@ class ApplicationsTabState {
}
}
@action
clearError(): void {
this.error = null;
if (this.navigationState === NavigationState.ERROR) {
@@ -14,7 +14,7 @@ import {
import type {SyncedPreferences, SyncedPreferencesField} from '@app/features/user/state/SyncedPreferencesEngine';
import {create, equals, type Message, type MessageInitShape, toBinary} from '@bufbuild/protobuf';
import type {GenMessage} from '@bufbuild/protobuf/codegenv2';
import {comparer, reaction, runInAction} from 'mobx';
import {compareShallow, reaction, runInAction} from 'mobx';
export {verifyRoundtripStability};
@@ -251,7 +251,7 @@ export async function makeSyncedField<
snapshot,
});
},
{fireImmediately: true, equals: comparer.shallow},
{fireImmediately: true, equals: compareShallow},
);
reaction(
() => (isEnabled() ? buildMessageOrSuspend('failed to build the local snapshot for observation') : null),
@@ -55,7 +55,7 @@ import camelCase from 'lodash/camelCase';
import isEqual from 'lodash/isEqual';
import isPlainObject from 'lodash/isPlainObject';
import snakeCase from 'lodash/snakeCase';
import {action, makeAutoObservable, reaction, runInAction} from 'mobx';
import {makeAutoObservable, reaction, runInAction} from 'mobx';
function restoreSettingValue<K extends keyof UserSettings>(target: UserSettings, source: UserSettings, key: K): void {
target[key] = source[key];
@@ -558,12 +558,10 @@ class UserSettingsState {
return this.hydrated;
}
@action
markSessionChanging(): void {
this.hydrated = false;
}
@action
handleAccountTransition(): void {
this.accountEpoch += 1;
this.hydrated = false;
@@ -590,7 +588,6 @@ class UserSettingsState {
}
}
@action
setStatus(status: StatusType): void {
this.status = status;
LocalPresence.updatePresence();
+1 -3
View File
@@ -3,7 +3,7 @@
import Authentication from '@app/features/auth/state/Authentication';
import {User} from '@app/features/user/models/User';
import type {UserPrivate, User as WireUser} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {action, makeAutoObservable, reaction, runInAction} from 'mobx';
import {makeAutoObservable, reaction, runInAction} from 'mobx';
const CURRENT_USER_PRIVATE_WIRE_KEYS = [
'is_staff',
@@ -92,7 +92,6 @@ class Users {
return this.usersList;
}
@action
handleGatewayReady(currentUser: UserPrivate): void {
const userRecord = new User(currentUser);
this.users = {
@@ -107,7 +106,6 @@ class Users {
}
}
@action
handleUserUpdate(
user: WireUser,
options?: {
@@ -96,7 +96,6 @@
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border: none;
border-radius: 0.875rem;
background-color: var(--mobile-video-hud-bg);
border: 0.0625rem solid var(--mobile-video-hud-border);
@@ -127,7 +126,6 @@
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border: none;
border-radius: 0.875rem;
background-color: var(--mobile-video-hud-bg);
border: 0.0625rem solid var(--mobile-video-hud-border);
@@ -74,7 +74,7 @@ function formatTransportSummary(transport: VoiceEngineV2TransportInfo | null): s
}
export function formatResolution(track: VoiceEngineV2PerTrackStats | null): string {
if (!track || !track.frameWidth || !track.frameHeight) return 'n/a';
if (!track?.frameWidth || !track.frameHeight) return 'n/a';
return `${track.frameWidth}x${track.frameHeight}`;
}
@@ -166,7 +166,7 @@ export function useTileContextMenuActive(tileElRef: React.RefObject<HTMLElement
const cm = ContextMenu.contextMenu;
const target = cm?.target?.target;
const el = tileElRef.current;
setOpen(Boolean(cm && target instanceof Node && el && el.contains(target)));
setOpen(Boolean(cm && target instanceof Node && el?.contains(target)));
});
return () => disposer();
}, [tileElRef]);
@@ -181,7 +181,7 @@ import type {
ScreenShareCaptureOptions,
TrackPublishOptions,
} from 'livekit-client';
import {makeObservable, observable} from 'mobx';
import {makeObservable, observableRef} from 'mobx';
const logger = new Logger('MediaEngineFacade');
@@ -403,7 +403,7 @@ class MediaEngineFacade extends Store {
setNativeAudioCaptureBridgeLifecycleBridge(this.voiceEngineV2SourceLifecycleBridge);
this.syncVoiceEngineV2AudioControlsFromAppState();
makeObservable<this, 'facadeSnapshot'>(this, {
facadeSnapshot: observable.ref,
facadeSnapshot: observableRef,
});
this.initializeEngineStoreSync();
this.initializeVoiceEngineV2ConnectionLifecycleSync();
@@ -199,7 +199,7 @@ export function parseScreenShareMigrationMessage(payload: Uint8Array): ScreenSha
}
}
export function encodeScreenShareMigrationMessage(message: ScreenShareMigrationMessage): Uint8Array {
export function encodeScreenShareMigrationMessage(message: ScreenShareMigrationMessage): Uint8Array<ArrayBuffer> {
return TEXT_ENCODER.encode(JSON.stringify(message));
}
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {makeObservable, observable, runInAction} from 'mobx';
import {makeObservable, observableRef, runInAction} from 'mobx';
import {useSyncExternalStore} from 'react';
export type StoreListener = () => void;
@@ -20,7 +20,7 @@ export class Store implements StoreLike {
constructor() {
makeObservable<this, 'storeVersion'>(this, {
storeVersion: observable.ref,
storeVersion: observableRef,
});
}
@@ -824,7 +824,7 @@ function subscribeToVoiceMediaGraphPublication<TFailure extends VoiceMediaGraphF
});
const commands = observerReplacementCommands(event, existing?.observedElement ?? null, event.observedElement);
if (event.hasPublication) {
if (!existing || !existing.publicationAvailable) {
if (!existing?.publicationAvailable) {
commands.push(subscribePublicationCommand(event, nextEntry));
} else {
const commandTarget = voiceMediaGraphSubscriptionCommandTarget(event);
@@ -993,7 +993,7 @@ function markVoiceMediaGraphSubscriptionPublicationMissing<TFailure extends Voic
target: VoiceMediaGraphSubscriptionTarget,
): VoiceMediaGraphSnapshot<TFailure> {
const entry = selectVoiceMediaGraphSubscriptionEntry(snapshot, target.participantIdentity, target.source);
if (!entry || !entry.publicationAvailable) return snapshot;
if (!entry?.publicationAvailable) return snapshot;
return setVoiceMediaGraphSubscriptionEntry(snapshot, updateSubscriptionAvailability(entry, false));
}
@@ -18,7 +18,7 @@ import {
type VoiceMediaGraphClockPort,
} from '@app/features/voice/engine/VoiceMediaGraphClock';
import {checkVoiceMediaGraphInvariants} from '@app/features/voice/engine/VoiceMediaGraphInvariants';
import {makeObservable, observable} from 'mobx';
import {makeObservable, observableRef} from 'mobx';
const logger = new Logger('VoiceMediaGraphStore');
const LOGGED_VIOLATION_LIMIT = 256;
@@ -33,7 +33,7 @@ export class VoiceMediaGraphStore extends Store {
super();
this.clock = clock;
makeObservable(this, {
graph: observable.ref,
graph: observableRef,
});
}
@@ -57,7 +57,7 @@ import type {
TrackPublishOptions,
} from 'livekit-client';
import {Room as LiveKitRoom, RoomEvent, Track} from 'livekit-client';
import {makeObservable, observable} from 'mobx';
import {makeObservable, observableRef} from 'mobx';
import type {Subscription} from 'rxjs';
import {timer} from 'rxjs';
@@ -214,8 +214,8 @@ export class VoiceEngineV2AppConnectionHostAdapter extends Store {
constructor() {
super();
makeObservable(this, {
connectionState: observable.ref,
hotSwapState: observable.ref,
connectionState: observableRef,
hotSwapState: observableRef,
});
this.throttle.subscribe(() => this.emitChange());
this.reconnect.subscribe(() => this.emitChange());
@@ -88,12 +88,11 @@ function buildOperatingError(method: string, reason: string): Error {
return error;
}
function toUint8Array(payload: ArrayBuffer | ArrayBufferView): Uint8Array {
if (payload instanceof Uint8Array) return payload;
if (ArrayBuffer.isView(payload)) {
return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
}
return new Uint8Array(payload);
function toUint8Array(payload: ArrayBuffer | ArrayBufferView): Uint8Array<ArrayBuffer> {
if (!ArrayBuffer.isView(payload)) return new Uint8Array(payload);
const {buffer, byteOffset, byteLength} = payload;
if (buffer instanceof ArrayBuffer) return new Uint8Array(buffer, byteOffset, byteLength);
return new Uint8Array(buffer, byteOffset, byteLength).slice();
}
function isDataPayload(value: unknown): value is ArrayBuffer | ArrayBufferView {
@@ -357,7 +357,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
async unmuteScreenShareAudioPublicationInternal(participant: LocalParticipant, reason: string): Promise<void> {
const publication = participant.getTrackPublication(Track.Source.ScreenShareAudio);
if (!publication || !publication.isMuted) {
if (!publication?.isMuted) {
return;
}
try {
@@ -771,7 +771,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
options: {includeSelfWindowAudio?: boolean; replaceExisting?: boolean} = {},
): Promise<boolean> {
const participant = room?.localParticipant;
if (!participant || !participant.isScreenShareEnabled) return false;
if (!participant?.isScreenShareEnabled) return false;
if (!linuxRule) return false;
if (options.replaceExisting !== true) {
const reconfigured = await reconfigureLinuxNativeAudioRouting(linuxRule, options);
@@ -807,7 +807,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
async ensureWindowScreenShareAudioPublication(room: Room | null, sourceId: string): Promise<boolean> {
const participant = room?.localParticipant;
if (!participant || !participant.isScreenShareEnabled) return false;
if (!participant?.isScreenShareEnabled) return false;
const targetPid = await getElectronAPI()
?.nativeAudio?.resolveAudioRootPidForSource(sourceId)
.catch((error) => {
@@ -842,7 +842,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
async ensureDeviceScreenShareMicPublication(room: Room | null, audioDeviceId: string): Promise<boolean> {
const participant = room?.localParticipant;
if (!participant || !participant.isScreenShareEnabled) return false;
if (!participant?.isScreenShareEnabled) return false;
await ensureNativeMicrophonePermissionForDeviceShare('replace');
const micTrack = await createLocalAudioTrack({
deviceId: audioDeviceId && audioDeviceId !== 'default' ? audioDeviceId : undefined,
@@ -1038,7 +1038,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
return false;
}
const participant = room?.localParticipant;
if (!participant || !participant.isScreenShareEnabled) return false;
if (!participant?.isScreenShareEnabled) return false;
const screenSharePublication = participant.getTrackPublication(Track.Source.ScreenShare);
const screenShareTrack = screenSharePublication?.videoTrack;
if (!screenShareTrack) {
@@ -689,7 +689,7 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows {
return false;
}
const participant = room?.localParticipant;
if (!participant || !participant.isScreenShareEnabled) {
if (!room || !participant?.isScreenShareEnabled) {
logger.warn('No active screen share to replace');
return false;
}
@@ -733,7 +733,7 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows {
return false;
}
const participant = room?.localParticipant;
if (!participant || !participant.isScreenShareEnabled) {
if (!room || !participant?.isScreenShareEnabled) {
logger.warn('No active screen share to replace');
return false;
}
@@ -772,7 +772,7 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows {
return false;
}
const participant = room?.localParticipant;
if (!room || !participant || !participant.isScreenShareEnabled) {
if (!room || !participant?.isScreenShareEnabled) {
logger.warn('No active screen share to republish');
return false;
}
@@ -892,7 +892,7 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows {
const sourceVideoTrack =
screenShareTrack.mediaStream?.getVideoTracks()[0] ??
(videoHadProcessor ? undefined : screenShareTrack.mediaStreamTrack);
if (!sourceVideoTrack || sourceVideoTrack.readyState !== 'live') {
if (sourceVideoTrack?.readyState !== 'live') {
throw new Error('Active screen share has no live source video track to preserve');
}
const audioPublications = getLocalScreenShareAudioPublications(participant);
@@ -169,7 +169,7 @@ type RoomWithEngine = Room & {
};
interface StatsSource {
getStats(): Promise<StatsReportMap>;
getStats(): Promise<StatsReportMap | undefined>;
getTransceivers?(): ReadonlyArray<StatsTransceiver>;
}
@@ -539,6 +539,7 @@ async function collectFromStatsSource(
transport: TransportInfo | null;
}> {
const reports = await source.getStats();
if (!reports) return {tracks: [], rtt: 0, transport: null};
const midToSenderTrackId = new Map<string, string>();
for (const transceiver of source.getTransceivers?.() ?? []) {
const senderTrackId = transceiver.sender?.track?.id;
@@ -16,7 +16,7 @@ import {
type VoiceGatewayVoiceStates,
} from '@app/features/voice/engine/VoiceGatewayStateMachine';
import {ME} from '@fluxer/constants/src/AppConstants';
import {makeObservable, observable} from 'mobx';
import {makeObservable, observable, observableRef} from 'mobx';
const logger = new Logger('VoiceEngineV2AppVoiceStateAdapter');
@@ -53,10 +53,10 @@ export class VoiceEngineV2AppVoiceStateAdapter extends Store {
constructor() {
super();
makeObservable<this, 'snapshot' | 'voiceStates' | 'userVoiceStates' | 'connectionVoiceStates'>(this, {
snapshot: observable.ref,
voiceStates: observable.ref,
userVoiceStates: observable.ref,
connectionVoiceStates: observable.ref,
snapshot: observableRef,
voiceStates: observableRef,
userVoiceStates: observableRef,
connectionVoiceStates: observableRef,
});
this.applyVoiceGatewayStateContext(this.snapshot.context);
}
@@ -29,7 +29,7 @@ async function hasScreenRecordingPermission(requestId: string): Promise<boolean>
export const useElectronScreenSharePicker = (): void => {
useEffect(() => {
const electronApi = getElectronAPI();
if (!electronApi || !electronApi.onDisplayMediaRequested) {
if (!electronApi?.onDisplayMediaRequested) {
logger.info('Screen share picker unavailable (missing platform handler)');
return;
}
@@ -25,7 +25,7 @@ export enum CallMode {
FULL_SCREEN = 'FULL_SCREEN',
}
export {CallLayout, type Call, type GatewayCallData};
export {type Call, CallLayout, type GatewayCallData};
class CallState {
calls = observable.map<string, Call>();
@@ -70,7 +70,7 @@ function attachToCaptureGraph(element: HTMLMediaElement): void {
function detachFromCaptureGraph(element: HTMLMediaElement): void {
const entry = routedElements.get(element);
if (!entry || !entry.connected) return;
if (!entry?.connected) return;
entry.connected = false;
element.removeEventListener('volumechange', entry.volumeListener);
try {
@@ -333,7 +333,7 @@ async function startLinuxNativeAudioCapture(
): Promise<NativeAudioStartResult | null> {
const electronApi = getElectronAPI();
const nativeAudioApi = electronApi?.nativeAudio;
if (!electronApi || electronApi.platform !== 'linux' || !nativeAudioApi) {
if (electronApi?.platform !== 'linux' || !nativeAudioApi) {
setLastArmFailure({
platform: electronApi?.platform,
reason: !electronApi
@@ -1078,7 +1078,7 @@ export async function armNativeAudioForLinuxRouting(
): Promise<boolean> {
const electronApi = getElectronAPI();
const nativeAudioApi = electronApi?.nativeAudio;
if (!electronApi || electronApi.platform !== 'linux' || !nativeAudioApi) {
if (electronApi?.platform !== 'linux' || !nativeAudioApi) {
setLastArmFailure({
platform: electronApi?.platform,
reason: !electronApi
@@ -6,7 +6,7 @@ import CallState from '@app/features/voice/state/CallState';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
export function isActiveCallParticipant(channel: Channel | null | undefined, userId: string): boolean {
if (!channel || !channel.isPrivate()) return false;
if (!channel?.isPrivate()) return false;
const call = CallState.getCall(channel.id);
if (!call) return false;
const participants = new Set([...call.participants, ...CallState.getParticipants(channel.id)]);
@@ -126,7 +126,7 @@ function buildLinuxNativeAudioRule(
export async function reconfigureActiveLinuxScreenShareAudioLink(): Promise<boolean> {
const electronApi = getElectronAPI();
const virtmicApi = electronApi?.virtmic;
if (!electronApi || electronApi.platform !== 'linux') {
if (electronApi?.platform !== 'linux') {
return false;
}
const sourceMode = VoiceSettings.getEffectiveScreenShareAudioSourceMode();
@@ -197,7 +197,7 @@ export async function reconfigureActiveLinuxAppShareAudio(
requestedWindowAudioScope?: WindowShareAudioScope,
): Promise<boolean> {
const electronApi = getElectronAPI();
if (!electronApi || electronApi.platform !== 'linux') return false;
if (electronApi?.platform !== 'linux') return false;
if (ActiveScreenShareSource.isOwnWindow()) {
logger.warn('Refusing to route audio into a Fluxer-owned window share');
await stopActiveLinuxScreenShareAudioLink();
@@ -244,7 +244,7 @@ export async function applyLiveScreenShareAudioSourceChange(
export async function stopActiveLinuxScreenShareAudioLink(): Promise<boolean> {
const electronApi = getElectronAPI();
const virtmicApi = electronApi?.virtmic;
if (!electronApi || electronApi.platform !== 'linux') {
if (electronApi?.platform !== 'linux') {
return false;
}
disarmVirtmic();
@@ -80,7 +80,7 @@ async function applyCameraVideoProcessor(
if (track.getProcessor() !== activeProcessor) {
throw new Error('Updated camera video processor is no longer attached to the active track');
}
if (!activeProcessor.processedTrack || activeProcessor.processedTrack.readyState !== 'live') {
if (activeProcessor.processedTrack?.readyState !== 'live') {
throw new Error('Updated camera video processor has no live output track');
}
return activeProcessor;
@@ -98,7 +98,7 @@ async function applyCameraVideoProcessor(
if (track.getProcessor() !== processor) {
throw new Error('Camera video processor was not retained by the active track');
}
if (!processor.processedTrack || processor.processedTrack.readyState !== 'live') {
if (processor.processedTrack?.readyState !== 'live') {
throw new Error('Camera video processor produced no live output track');
}
logger.info(logLabel);