mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
chore(deps): upgrade all dependencies, toolchains and images (#2825)
This commit is contained in:
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;
|
||||
|
||||
+1
-1
@@ -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();
|
||||
});
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+12
-12
@@ -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},
|
||||
|
||||
+4
-4
@@ -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 = {};
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
);
|
||||
|
||||
@@ -131,7 +131,7 @@ class PiP {
|
||||
}
|
||||
}
|
||||
|
||||
export {PIP_DEFAULT_WIDTH};
|
||||
export type {PiPContent, PiPContentType, PiPCorner};
|
||||
export {PIP_DEFAULT_WIDTH};
|
||||
|
||||
export default new PiP();
|
||||
@@ -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,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 {
|
||||
|
||||
+1
-2
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+5
-5
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user