[skip ci] feat: prepare for public release

This commit is contained in:
Hampus Kraft
2026-01-02 19:27:51 +00:00
parent 197b23757f
commit 5ae825fc7d
199 changed files with 38391 additions and 33358 deletions

View File

@@ -19,7 +19,6 @@
import {makeAutoObservable, observable, reaction, runInAction} from 'mobx';
import {ChannelTypes, ME, Permissions} from '~/Constants';
import {Logger} from '~/lib/Logger';
import ChannelStore from './ChannelStore';
import GuildStore from './GuildStore';
import PermissionStore from './PermissionStore';
@@ -32,8 +31,6 @@ type ChannelId = string;
const PRIVATE_CHANNEL_SENTINEL = ME;
const CAN_READ_PERMISSIONS = Permissions.VIEW_CHANNEL | Permissions.READ_MESSAGE_HISTORY;
const _logger = new Logger('GuildReadStateStore');
class GuildReadState {
unread = observable.box(false);
unreadChannelId = observable.box<ChannelId | null>(null);

View File

@@ -18,9 +18,7 @@
*/
import {action, makeAutoObservable, reaction, runInAction} from 'mobx';
import * as ModalActionCreators from '~/actions/ModalActionCreators';
import {modal} from '~/actions/ModalActionCreators';
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
import {type User, type UserPrivate, UserRecord} from '~/records/UserRecord';
import AuthenticationStore from '~/stores/AuthenticationStore';
@@ -73,7 +71,7 @@ class UserStore {
if (!userRecord.isClaimed()) {
setTimeout(async () => {
ModalActionCreators.push(modal(() => <ClaimAccountModal />));
openClaimAccountModal();
}, 1000);
}
}

View File

@@ -86,6 +86,7 @@ class ConnectionStore {
handleGatewayDispatch: action.bound,
ensureGuildActiveAndSynced: action.bound,
flushPendingGuildSync: action.bound,
syncGuildIfNeeded: action.bound,
},
{autoBind: true},
);
@@ -324,6 +325,10 @@ class ConnectionStore {
}
}
syncGuildIfNeeded(guildId: string, reason?: string): void {
this.ensureGuildActiveAndSynced(guildId, {reason});
}
private flushPendingGuildSync(): void {
const guildId = this.pendingGuildSyncId ?? SelectedGuildStore.selectedGuildId;
if (!guildId || guildId === FAVORITES_GUILD_ID) {

View File

@@ -23,19 +23,22 @@ import type {Participant, Room, ScreenShareCaptureOptions, TrackPublishOptions}
import {computed, makeObservable} from 'mobx';
import * as SoundActionCreators from '~/actions/SoundActionCreators';
import * as ToastActionCreators from '~/actions/ToastActionCreators';
import {type GatewayErrorCode, GatewayErrorCodes} from '~/Constants';
import {ChannelTypes, type GatewayErrorCode, GatewayErrorCodes} from '~/Constants';
import type {GatewayErrorData} from '~/lib/GatewaySocket';
import {Logger} from '~/lib/Logger';
import {voiceStatsDB} from '~/lib/VoiceStatsDB';
import type {GuildReadyData} from '~/records/GuildRecord';
import AuthenticationStore from '~/stores/AuthenticationStore';
import CallMediaPrefsStore from '~/stores/CallMediaPrefsStore';
import ChannelStore from '~/stores/ChannelStore';
import ConnectionStore from '~/stores/ConnectionStore';
import GuildMemberStore from '~/stores/GuildMemberStore';
import GuildStore from '~/stores/GuildStore';
import IdleStore from '~/stores/IdleStore';
import LocalVoiceStateStore from '~/stores/LocalVoiceStateStore';
import MediaPermissionStore from '~/stores/MediaPermissionStore';
import UserStore from '~/stores/UserStore';
import VoiceDevicePermissionStore from '~/stores/voice/VoiceDevicePermissionStore';
import {SoundType} from '~/utils/SoundUtils';
import {
checkChannelLimit,
@@ -152,6 +155,33 @@ class MediaEngineFacade {
});
return;
}
const currentUser = UserStore.getCurrentUser();
const isUnclaimed = !(currentUser?.isClaimed() ?? false);
if (isUnclaimed) {
if (!this.i18n) {
throw new Error('MediaEngineFacade: i18n not initialized');
}
if (guildId) {
const guild = GuildStore.getGuild(guildId);
const isOwner = guild?.isOwner(currentUserId) ?? false;
if (!isOwner) {
ToastActionCreators.createToast({
type: 'error',
children: this.i18n._(msg`Claim your account to join voice channels you don't own.`),
});
return;
}
} else {
const channel = ChannelStore.getChannel(channelId);
if (channel?.type === ChannelTypes.DM) {
ToastActionCreators.createToast({
type: 'error',
children: this.i18n._(msg`Claim your account to start or join 1:1 calls.`),
});
return;
}
}
}
if (!ConnectionStore.socket) {
logger.warn('[connectToVoiceChannel] No socket');
return;
@@ -159,6 +189,8 @@ class MediaEngineFacade {
if (!checkChannelLimit(guildId, channelId)) return;
this.voiceStateSync.reset();
const shouldProceed = checkMultipleConnections(
guildId,
channelId,
@@ -185,6 +217,7 @@ class MediaEngineFacade {
await this.disconnectFromVoiceChannel('user');
}
}
this.voiceStateSync.reset();
VoiceConnectionManager.startConnection(guildId, channelId);
sendVoiceStateConnect(guildId, channelId);
}
@@ -298,17 +331,29 @@ class MediaEngineFacade {
const {guildId, channelId, connectionId} = VoiceConnectionManager.connectionState;
if (!channelId || !connectionId) return;
const devicePermission = VoiceDevicePermissionStore.getState().permissionStatus;
const micGranted = MediaPermissionStore.isMicrophoneGranted() || devicePermission === 'granted';
const payload: VoiceStateSyncPayload = {
guild_id: guildId,
channel_id: channelId,
connection_id: connectionId,
self_mute: partial?.self_mute ?? LocalVoiceStateStore.getSelfMute(),
self_mute:
micGranted && partial?.self_mute !== undefined
? partial.self_mute
: micGranted
? LocalVoiceStateStore.getSelfMute()
: true,
self_deaf: partial?.self_deaf ?? LocalVoiceStateStore.getSelfDeaf(),
self_video: partial?.self_video ?? LocalVoiceStateStore.getSelfVideo(),
self_stream: partial?.self_stream ?? LocalVoiceStateStore.getSelfStream(),
viewer_stream_key: partial?.viewer_stream_key ?? LocalVoiceStateStore.getViewerStreamKey(),
};
if (!micGranted && !LocalVoiceStateStore.getSelfMute()) {
LocalVoiceStateStore.updateSelfMute(true);
}
this.voiceStateSync.requestState(payload);
}
@@ -451,6 +496,7 @@ class MediaEngineFacade {
GatewayErrorCodes.VOICE_CHANNEL_FULL,
GatewayErrorCodes.VOICE_MISSING_CONNECTION_ID,
GatewayErrorCodes.VOICE_TOKEN_FAILED,
GatewayErrorCodes.VOICE_UNCLAIMED_ACCOUNT,
]);
if (!voiceErrorCodes.has(error.code)) {
@@ -467,7 +513,8 @@ class MediaEngineFacade {
} else if (
error.code === GatewayErrorCodes.VOICE_PERMISSION_DENIED ||
error.code === GatewayErrorCodes.VOICE_CHANNEL_FULL ||
error.code === GatewayErrorCodes.VOICE_MEMBER_TIMED_OUT
error.code === GatewayErrorCodes.VOICE_MEMBER_TIMED_OUT ||
error.code === GatewayErrorCodes.VOICE_UNCLAIMED_ACCOUNT
) {
if (VoiceConnectionManager.connecting && !VoiceConnectionManager.connected) {
logger.info('[handleGatewayError] Permission denied, channel full, or timeout while connecting, aborting');
@@ -481,6 +528,14 @@ class MediaEngineFacade {
type: 'error',
children: this.i18n._(msg`You can't join while you're on timeout.`),
});
} else if (error.code === GatewayErrorCodes.VOICE_UNCLAIMED_ACCOUNT) {
if (!this.i18n) {
throw new Error('MediaEngineFacade: i18n not initialized');
}
ToastActionCreators.createToast({
type: 'error',
children: this.i18n._(msg`Claim your account to join this voice channel.`),
});
}
} else if (error.code === GatewayErrorCodes.VOICE_TOKEN_FAILED) {
if (VoiceConnectionManager.connecting) {

View File

@@ -36,18 +36,22 @@ const logger = new Logger('VoiceStateSyncManager');
export class VoiceStateSyncManager {
private pending: VoiceStateSyncPayload | null = null;
private lastSent: VoiceStateSyncPayload | null = null;
private inFlight: VoiceStateSyncPayload | null = null;
private serverState: VoiceStateSyncPayload | null = null;
private flushScheduled = false;
reset(): void {
this.pending = null;
this.lastSent = null;
this.inFlight = null;
this.serverState = null;
this.flushScheduled = false;
logger.debug('[reset] Voice state sync cache cleared');
}
requestState(payload: VoiceStateSyncPayload): void {
this.pending = payload;
this.flush();
this.scheduleFlush();
}
confirmServerState(payload: VoiceStateSyncPayload | null): void {
@@ -57,14 +61,46 @@ export class VoiceStateSyncManager {
}
this.serverState = payload;
if (this.inFlight && this.areEqual(this.inFlight, payload)) {
this.inFlight = null;
this.lastSent = payload;
}
if (this.pending && this.areEqual(this.pending, payload)) {
this.pending = null;
}
this.scheduleFlush();
}
private scheduleFlush(): void {
if (this.flushScheduled) return;
this.flushScheduled = true;
Promise.resolve().then(() => {
this.flushScheduled = false;
this.flush();
});
}
private flush(): void {
if (!this.pending) return;
if (
this.inFlight &&
(this.inFlight.connection_id !== this.pending.connection_id ||
this.inFlight.channel_id !== this.pending.channel_id ||
this.inFlight.guild_id !== this.pending.guild_id)
) {
logger.debug('[flush] Dropping stale in-flight state after context change', {
inFlight: this.inFlight,
pending: this.pending,
});
this.inFlight = null;
}
if (this.inFlight) return;
if (this.lastSent && this.areEqual(this.lastSent, this.pending)) {
this.pending = null;
return;
@@ -86,6 +122,7 @@ export class VoiceStateSyncManager {
});
this.lastSent = this.pending;
this.inFlight = this.pending;
this.pending = null;
}