mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
refactor(app): name mute and unread state accessors by intent (#1859)
This commit is contained in:
1 parent
52781cfa2d
commit
a2099fb0e2
33 files changed
+173
-490
No files matched your search
@@ -75,8 +75,8 @@ const setBadge = (badge: number): void => {
|
||||
export const AppBadge: React.FC = observer(() => {
|
||||
const relationships = Relationships.getRelationships();
|
||||
const unreadMessageBadgeEnabled = Notification.unreadMessageBadgeEnabled;
|
||||
const mentionCount = GuildReadState.getTotalMentionCount();
|
||||
const hasUnread = GuildReadState.hasAnyUnread;
|
||||
const mentionCount = GuildReadState.mentionCountAcrossGuilds();
|
||||
const hasUnread = GuildReadState.anyGuildUnread;
|
||||
const pendingCount = RuntimeConfig.directMessagesDisabled
|
||||
? 0
|
||||
: relationships.filter((relationship) => relationship.type === RelationshipTypes.INCOMING_REQUEST).length;
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export const FavoritesGuildHeaderBottomSheet: React.FC<FavoritesGuildHeaderBotto
|
||||
({isOpen, onClose}) => {
|
||||
const {i18n} = useLingui();
|
||||
const hideMutedChannels = Favorites.hideMutedChannels;
|
||||
const settings = UserGuildSettings.getSettings(FAVORITES_GUILD_ID);
|
||||
const settings = UserGuildSettings.getSettingsForScope(FAVORITES_GUILD_ID);
|
||||
const isMuted = settings?.muted ?? false;
|
||||
const handleAddChannel = () => {
|
||||
ModalCommands.pushAfterBottomSheetClose(
|
||||
|
||||
@@ -360,7 +360,7 @@ const UnreadChannelCard = observer(function UnreadChannelCard({
|
||||
const messageGroupSpacing = Accessibility.getMessageGroupSpacingValue(messageDisplayCompact);
|
||||
const mentionCount = ReadStates.getMentionCount(channel.id);
|
||||
const isCollapsed = UnreadChannels.isCollapsed(channel.id);
|
||||
const isMuted = UserGuildSettings.isGuildOrCategoryOrChannelMuted(channel.guildId ?? null, channel.id);
|
||||
const isMuted = UserGuildSettings.isMutedAtAnyLevel(channel.guildId ?? null, channel.id);
|
||||
const previewMessages = previewData?.messages ?? [];
|
||||
const oldestUnreadMessageId = previewData?.oldestUnreadMessageId ?? null;
|
||||
const totalUnreadCount = ReadStates.getUnreadCount(channel.id);
|
||||
|
||||
@@ -238,7 +238,7 @@ export const ChannelItem = observer(
|
||||
const canInvite = InviteUtils.canInviteToChannel(channel.id, channel.guildId);
|
||||
const mobileLayout = MobileLayout;
|
||||
const isMuted = UserGuildSettings.isGuildOrChannelMuted(guild.id, channel.id);
|
||||
const isChannelDirectlyMuted = UserGuildSettings.isChannelMuted(guild.id, channel.id);
|
||||
const isChannelDirectlyMuted = UserGuildSettings.isChannelDirectlyMuted(guild.id, channel.id);
|
||||
const currentUserCount =
|
||||
channelIsVoice && channel.userLimit != null && channel.userLimit > 0
|
||||
? Object.keys(MediaEngine.getAllVoiceStatesInChannel(guild.id, channel.id)).length
|
||||
|
||||
@@ -164,7 +164,7 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
||||
const {i18n} = useLingui();
|
||||
const channels = Channels.getGuildChannels(guild.id);
|
||||
const location = useLocation();
|
||||
const userGuildSettings = UserGuildSettings.getSettings(guild.id);
|
||||
const userGuildSettings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const isDraggingAnything = useDragLayer((monitor) => {
|
||||
if (!monitor.isDragging()) return false;
|
||||
const itemType = monitor.getItemType();
|
||||
@@ -337,7 +337,7 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
||||
const unreadCount = ReadStates.getUnreadCount(channelId);
|
||||
const mentionCount = ReadStates.getMentionCount(channelId);
|
||||
const isMuted =
|
||||
UserGuildSettings.isCategoryMuted(guild.id, channelId) || UserGuildSettings.isChannelMuted(guild.id, channelId);
|
||||
UserGuildSettings.isParentCategoryMuted(guild.id, channelId) || UserGuildSettings.isChannelDirectlyMuted(guild.id, channelId);
|
||||
const channel = Channels.getChannel(channelId);
|
||||
const unreadBadgesLevel = channel
|
||||
? UserGuildSettings.resolvedUnreadBadgesLevel({
|
||||
@@ -360,13 +360,13 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
||||
for (const group of channelGroups) {
|
||||
const isCollapsed = group.category ? (collapsedCategories?.has(group.category.id) ?? false) : false;
|
||||
const isNullSpace = !group.category;
|
||||
const isCategoryMuted = group.category ? UserGuildSettings.isChannelMuted(guild.id, group.category.id) : false;
|
||||
const isCategoryMuted = group.category ? UserGuildSettings.isChannelDirectlyMuted(guild.id, group.category.id) : false;
|
||||
let filteredTextChannels: Array<Channel>;
|
||||
let filteredVoiceChannels: Array<Channel>;
|
||||
if (hideMutedChannels) {
|
||||
filteredTextChannels = [];
|
||||
for (const ch of group.textChannels) {
|
||||
const isMuted = UserGuildSettings.isChannelMuted(guild.id, ch.id);
|
||||
const isMuted = UserGuildSettings.isChannelDirectlyMuted(guild.id, ch.id);
|
||||
if (
|
||||
shouldShowChannelWhenHidingMutedChannels({
|
||||
isMuted,
|
||||
@@ -380,7 +380,7 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
||||
}
|
||||
filteredVoiceChannels = [];
|
||||
for (const ch of group.voiceChannels) {
|
||||
const isMuted = UserGuildSettings.isChannelMuted(guild.id, ch.id);
|
||||
const isMuted = UserGuildSettings.isChannelDirectlyMuted(guild.id, ch.id);
|
||||
if (
|
||||
shouldShowChannelWhenHidingMutedChannels({
|
||||
isMuted,
|
||||
|
||||
@@ -206,11 +206,11 @@ const FavoriteChannelResolvedItem = observer(
|
||||
const channelDisplayName = channel.isPrivate() ? ChannelUtils.getDMDisplayName(channel) : channel.name;
|
||||
const displayName = favoriteChannel.nickname || channelDisplayName || i18n._(UNKNOWN_CHANNEL_DESCRIPTOR);
|
||||
const isChannelDirectlyMuted = channel.guildId
|
||||
? UserGuildSettings.isChannelMuted(channel.guildId, channel.id)
|
||||
? UserGuildSettings.isChannelDirectlyMuted(channel.guildId, channel.id)
|
||||
: false;
|
||||
const isMuted =
|
||||
isChannelDirectlyMuted ||
|
||||
(channel.guildId ? UserGuildSettings.isCategoryMuted(channel.guildId, channel.id) : false);
|
||||
(channel.guildId ? UserGuildSettings.isParentCategoryMuted(channel.guildId, channel.id) : false);
|
||||
const unreadBadgesLevel = channel.guildId
|
||||
? UserGuildSettings.resolvedUnreadBadgesLevel({
|
||||
id: channel.id,
|
||||
|
||||
@@ -665,7 +665,7 @@ function getDMScrollIndicatorSeverity(channelId: string): ScrollIndicatorSeverit
|
||||
const unreadState = getChannelUnreadState({
|
||||
unreadCount: ReadStates.getPrivateChannelUnreadCount(channelId),
|
||||
mentionCount,
|
||||
isMuted: UserGuildSettings.isChannelMuted(null, channelId),
|
||||
isMuted: UserGuildSettings.isChannelDirectlyMuted(null, channelId),
|
||||
showFadedUnreadOnMutedChannels: Accessibility.showFadedUnreadOnMutedChannels,
|
||||
});
|
||||
if (mentionCount > 0) return 'mention';
|
||||
@@ -674,8 +674,8 @@ function getDMScrollIndicatorSeverity(channelId: string): ScrollIndicatorSeverit
|
||||
}
|
||||
|
||||
function getGuildScrollIndicatorSeverity(guildId: string): ScrollIndicatorSeverity | null {
|
||||
if (GuildReadState.getMentionCount(guildId) > 0) return 'mention';
|
||||
if (GuildReadState.hasUnread(guildId)) return 'unread';
|
||||
if (GuildReadState.mentionCountForGuild(guildId) > 0) return 'mention';
|
||||
if (GuildReadState.guildIsUnread(guildId)) return 'unread';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1327,10 +1327,10 @@ const GUILD_RAIL_INDICATORS_BY_TOKEN: Record<string, SkeletonGuildRailItemIndica
|
||||
function resolveGuildRailIndicator(guildIds: ReadonlyArray<string>): SkeletonGuildRailItemIndicator {
|
||||
let hasUnread = false;
|
||||
for (const guildId of guildIds) {
|
||||
if (GuildReadState.getMentionCount(guildId) > 0) {
|
||||
if (GuildReadState.mentionCountForGuild(guildId) > 0) {
|
||||
return SkeletonGuildRailItemIndicator.MENTION;
|
||||
}
|
||||
if (GuildReadState.hasUnread(guildId)) {
|
||||
if (GuildReadState.guildIsUnread(guildId)) {
|
||||
hasUnread = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,8 +316,8 @@ export const GuildFolderItem = observer((props: GuildFolderItemProps) => {
|
||||
}) as CSSProperties,
|
||||
[folderColor],
|
||||
);
|
||||
const hasUnreadMessages = guilds.some((guild) => GuildReadState.hasUnread(guild.id));
|
||||
const totalMentionCount = guilds.reduce((sum, guild) => sum + GuildReadState.getMentionCount(guild.id), 0);
|
||||
const hasUnreadMessages = guilds.some((guild) => GuildReadState.guildIsUnread(guild.id));
|
||||
const totalMentionCount = guilds.reduce((sum, guild) => sum + GuildReadState.mentionCountForGuild(guild.id), 0);
|
||||
let folderScrollTargetRef: React.RefCallback<HTMLElement> | null = null;
|
||||
if (registerScrollTarget != null) {
|
||||
folderScrollTargetRef = registerScrollTarget(folderId);
|
||||
|
||||
@@ -72,7 +72,7 @@ export function resolveDMListItemUnreadState(channelId: string): ChannelUnreadSt
|
||||
return getChannelUnreadState({
|
||||
unreadCount: ReadStates.getUnreadCount(channelId),
|
||||
mentionCount: ReadStates.getMentionCount(channelId),
|
||||
isMuted: UserGuildSettings.isChannelMuted(null, channelId),
|
||||
isMuted: UserGuildSettings.isChannelDirectlyMuted(null, channelId),
|
||||
showFadedUnreadOnMutedChannels: Accessibility.showFadedUnreadOnMutedChannels,
|
||||
unreadBadgesLevel: null,
|
||||
});
|
||||
@@ -124,7 +124,7 @@ const ResolvedDMListItem = observer(function ResolvedDMListItem({
|
||||
const contextMenuOpen = useContextMenuHoverState(buttonRef, !isMobileExperience);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const {keyboardModeEnabled} = KeyboardMode;
|
||||
const isMuted = UserGuildSettings.isChannelMuted(null, channel.id);
|
||||
const isMuted = UserGuildSettings.isChannelDirectlyMuted(null, channel.id);
|
||||
const mentionCount = ReadStates.getMentionCount(channel.id);
|
||||
const unreadState = resolveDMListItemUnreadState(channel.id);
|
||||
const directMessageName = recipient ? NicknameUtils.getNickname(recipient, null, channel.id) : null;
|
||||
|
||||
@@ -107,9 +107,9 @@ export function useGuildListItemState({
|
||||
isSelected,
|
||||
selectedGuildIndex,
|
||||
}: UseGuildListItemStateOptions): GuildListItemState {
|
||||
const hasUnreadMessages = GuildReadState.hasUnread(guild.id);
|
||||
const mentionCount = GuildReadState.getMentionCount(guild.id);
|
||||
const settings = UserGuildSettings.getSettings(guild.id);
|
||||
const hasUnreadMessages = GuildReadState.guildIsUnread(guild.id);
|
||||
const mentionCount = GuildReadState.mentionCountForGuild(guild.id);
|
||||
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const isMuted = settings.muted;
|
||||
const muteConfig = settings.mute_config;
|
||||
let muteEndTime: string | null = null;
|
||||
|
||||
@@ -132,7 +132,7 @@ const ResolvedDMListItem = observer(function ResolvedDMListItem({
|
||||
: TypingIndicator.isTyping(channel.id, recipientId);
|
||||
const hasUnreadMessages = () => ReadStates.hasUnread(channel.id);
|
||||
const isMobile = MobileLayout.isMobileLayout();
|
||||
const isMuted = UserGuildSettings.isChannelMuted(null, channel.id);
|
||||
const isMuted = UserGuildSettings.isChannelDirectlyMuted(null, channel.id);
|
||||
const mentionCount = ReadStates.getMentionCount(channel.id);
|
||||
const unreadState = getChannelUnreadState({
|
||||
unreadCount: ReadStates.getUnreadCount(channel.id),
|
||||
|
||||
@@ -166,7 +166,7 @@ export const GuildNotificationSettingsModal = observer(({guildId}: {guildId: str
|
||||
const {i18n} = useLingui();
|
||||
const muteChannelLabel = i18n._(MUTE_CHANNEL_DESCRIPTOR);
|
||||
const guild = Guilds.getGuild(guildId);
|
||||
const settings = UserGuildSettings.getSettings(guildId);
|
||||
const settings = UserGuildSettings.getSettingsForScope(guildId);
|
||||
if (!guild || !settings) return null;
|
||||
const channels = Channels.getGuildChannels(guildId);
|
||||
const categories = channels.filter((c) => c.type === ChannelTypes.GUILD_CATEGORY);
|
||||
|
||||
@@ -171,11 +171,11 @@ export const GuildHeaderPopout = observer(({guild}: {guild: Guild}) => {
|
||||
canCreateExpressions ||
|
||||
canBanMembers;
|
||||
const canEditCommunityProfile = Users.getCurrentUser()?.isClaimed() ?? true;
|
||||
const settings = UserGuildSettings.getSettings(guild.id);
|
||||
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const hideMutedChannels = settings?.hide_muted_channels ?? false;
|
||||
const handleToggleHideMutedChannels = useCallback(
|
||||
(checked: boolean) => {
|
||||
const currentSettings = UserGuildSettings.getSettings(guild.id);
|
||||
const currentSettings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
||||
if (checked === currentValue) return;
|
||||
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
||||
|
||||
@@ -20,18 +20,18 @@ const PRIVATE_CHANNEL_SENTINEL = ME;
|
||||
|
||||
class GuildReadStateEntry {
|
||||
unread = observable.box(false);
|
||||
unreadChannelId = observable.box<ChannelId | null>(null);
|
||||
firstUnreadChannelId = observable.box<ChannelId | null>(null);
|
||||
mentionCount = observable.box(0);
|
||||
mentionChannels = observable.set(new Set<ChannelId>());
|
||||
sentinel = observable.box(0);
|
||||
changeTicket = observable.box(0);
|
||||
|
||||
incrementSentinel(): void {
|
||||
this.sentinel.set(this.sentinel.get() + 1);
|
||||
bumpChangeTicket(): void {
|
||||
this.changeTicket.set(this.changeTicket.get() + 1);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.unread.set(false);
|
||||
this.unreadChannelId.set(null);
|
||||
this.firstUnreadChannelId.set(null);
|
||||
this.mentionCount.set(0);
|
||||
this.mentionChannels.clear();
|
||||
}
|
||||
@@ -43,12 +43,12 @@ type ContributeChannel = {
|
||||
guildId?: string | null;
|
||||
parentId?: string | null;
|
||||
isPrivate(): boolean;
|
||||
isGuildVocal?(): boolean;
|
||||
isGuildAudioChannel?(): boolean;
|
||||
};
|
||||
|
||||
function isChannelMutedForUnread(channel: ContributeChannel): boolean {
|
||||
if (channel.isPrivate()) return false;
|
||||
return UserGuildSettings.isGuildOrCategoryOrChannelMuted(channel.guildId ?? null, channel.id);
|
||||
return UserGuildSettings.isMutedAtAnyLevel(channel.guildId ?? null, channel.id);
|
||||
}
|
||||
|
||||
function resolveUnreadBadgesLevel(channel: ContributeChannel): number | null {
|
||||
@@ -77,7 +77,7 @@ function getChannelContribution(channel: ContributeChannel, channelId: string):
|
||||
|
||||
class GuildReadState {
|
||||
private readonly guildStates = observable.map(new Map<GuildId, GuildReadStateEntry>());
|
||||
private readonly unreadGuilds = observable.set(new Set<GuildId>());
|
||||
private readonly unreadGuildIds = observable.set(new Set<GuildId>());
|
||||
updateCounter = 0;
|
||||
private readStateReactionInstalled = false;
|
||||
|
||||
@@ -156,9 +156,9 @@ class GuildReadState {
|
||||
this.updateCounter++;
|
||||
}
|
||||
|
||||
private incrementSentinel(guildId: string | null): void {
|
||||
private bumpChangeTicket(guildId: string | null): void {
|
||||
const state = this.getOrCreate(guildId);
|
||||
state.incrementSentinel();
|
||||
state.bumpChangeTicket();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
@@ -166,18 +166,18 @@ class GuildReadState {
|
||||
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
||||
const state = this.getOrCreate(id);
|
||||
const previousUnread = state.unread.get();
|
||||
const previousUnreadChannelId = state.unreadChannelId.get();
|
||||
const previousFirstUnread = state.firstUnreadChannelId.get();
|
||||
const previousMentionCount = state.mentionCount.get();
|
||||
const mentionChannelUpdates = new Map<ChannelId, boolean>();
|
||||
let foundUnread = false;
|
||||
let shouldClearUnreadChannelId = false;
|
||||
let nextUnreadChannelId = previousUnreadChannelId;
|
||||
let shouldClearFirstUnread = false;
|
||||
let nextFirstUnread = previousFirstUnread;
|
||||
for (const channelId of channelIds) {
|
||||
const channel = Channels.getChannel(channelId);
|
||||
if (channel == null) {
|
||||
mentionChannelUpdates.set(channelId, false);
|
||||
if (nextUnreadChannelId === channelId) {
|
||||
shouldClearUnreadChannelId = true;
|
||||
if (nextFirstUnread === channelId) {
|
||||
shouldClearFirstUnread = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -194,14 +194,14 @@ class GuildReadState {
|
||||
mentionChannelUpdates.set(channelId, contribution.mentionAllowed);
|
||||
if (guildId != null && !foundUnread && contribution.unreadAllowed) {
|
||||
foundUnread = true;
|
||||
nextUnreadChannelId = channelId;
|
||||
} else if (!contribution.unreadAllowed && nextUnreadChannelId === channelId) {
|
||||
shouldClearUnreadChannelId = true;
|
||||
nextFirstUnread = channelId;
|
||||
} else if (!contribution.unreadAllowed && nextFirstUnread === channelId) {
|
||||
shouldClearFirstUnread = true;
|
||||
}
|
||||
}
|
||||
const nextUnread = foundUnread;
|
||||
if (!nextUnread && shouldClearUnreadChannelId) {
|
||||
nextUnreadChannelId = null;
|
||||
if (!nextUnread && shouldClearFirstUnread) {
|
||||
nextFirstUnread = null;
|
||||
}
|
||||
if (previousUnread && !nextUnread) {
|
||||
return this.recomputeAll(guildId);
|
||||
@@ -219,9 +219,7 @@ class GuildReadState {
|
||||
}
|
||||
}
|
||||
const changed =
|
||||
nextUnread !== previousUnread ||
|
||||
nextUnreadChannelId !== previousUnreadChannelId ||
|
||||
mentionTotal !== previousMentionCount;
|
||||
nextUnread !== previousUnread || nextFirstUnread !== previousFirstUnread || mentionTotal !== previousMentionCount;
|
||||
if (!changed && mentionChannelUpdates.size === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -237,16 +235,16 @@ class GuildReadState {
|
||||
return;
|
||||
}
|
||||
state.unread.set(nextUnread);
|
||||
state.unreadChannelId.set(nextUnreadChannelId);
|
||||
state.firstUnreadChannelId.set(nextFirstUnread);
|
||||
state.mentionCount.set(mentionTotal);
|
||||
if (id !== PRIVATE_CHANNEL_SENTINEL) {
|
||||
if (nextUnread) {
|
||||
this.unreadGuilds.add(id as GuildId);
|
||||
this.unreadGuildIds.add(id as GuildId);
|
||||
} else {
|
||||
this.unreadGuilds.delete(id as GuildId);
|
||||
this.unreadGuildIds.delete(id as GuildId);
|
||||
}
|
||||
}
|
||||
this.incrementSentinel(guildId);
|
||||
this.bumpChangeTicket(guildId);
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
@@ -264,11 +262,11 @@ class GuildReadState {
|
||||
}
|
||||
if (!newState.unread.get() && contribution.unreadAllowed) {
|
||||
newState.unread.set(true);
|
||||
newState.unreadChannelId.set(channel.id as ChannelId);
|
||||
newState.firstUnreadChannelId.set(channel.id as ChannelId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const isGuildMuted = UserGuildSettings.isMuted(guildId);
|
||||
const isGuildMuted = UserGuildSettings.isWholeGuildMuted(guildId);
|
||||
if (isGuildMuted && skipIfMuted) {
|
||||
return false;
|
||||
}
|
||||
@@ -281,7 +279,7 @@ class GuildReadState {
|
||||
}
|
||||
if (!newState.unread.get() && contribution.unreadAllowed) {
|
||||
newState.unread.set(true);
|
||||
newState.unreadChannelId.set(channel.id as ChannelId);
|
||||
newState.firstUnreadChannelId.set(channel.id as ChannelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +290,7 @@ class GuildReadState {
|
||||
private commitState(guildId: string, newState: GuildReadStateEntry, prevState: GuildReadStateEntry): boolean {
|
||||
if (
|
||||
newState.unread.get() === prevState.unread.get() &&
|
||||
newState.unreadChannelId.get() === prevState.unreadChannelId.get() &&
|
||||
newState.firstUnreadChannelId.get() === prevState.firstUnreadChannelId.get() &&
|
||||
newState.mentionCount.get() === prevState.mentionCount.get()
|
||||
) {
|
||||
return false;
|
||||
@@ -301,12 +299,12 @@ class GuildReadState {
|
||||
this.guildStates.set(guildId as GuildId, newState);
|
||||
if (guildId !== PRIVATE_CHANNEL_SENTINEL) {
|
||||
if (newState.unread.get()) {
|
||||
this.unreadGuilds.add(guildId as GuildId);
|
||||
this.unreadGuildIds.add(guildId as GuildId);
|
||||
} else {
|
||||
this.unreadGuilds.delete(guildId as GuildId);
|
||||
this.unreadGuildIds.delete(guildId as GuildId);
|
||||
}
|
||||
}
|
||||
this.incrementSentinel(guildId === PRIVATE_CHANNEL_SENTINEL ? null : guildId);
|
||||
this.bumpChangeTicket(guildId === PRIVATE_CHANNEL_SENTINEL ? null : guildId);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -335,21 +333,21 @@ class GuildReadState {
|
||||
}
|
||||
}
|
||||
|
||||
get hasAnyUnread(): boolean {
|
||||
return this.unreadGuilds.size > 0;
|
||||
get anyGuildUnread(): boolean {
|
||||
return this.unreadGuildIds.size > 0;
|
||||
}
|
||||
|
||||
hasUnread(guildId: string): boolean {
|
||||
return this.unreadGuilds.has(guildId as GuildId);
|
||||
guildIsUnread(guildId: string): boolean {
|
||||
return this.unreadGuildIds.has(guildId as GuildId);
|
||||
}
|
||||
|
||||
getMentionCount(guildId: string | null): number {
|
||||
mentionCountForGuild(guildId: string | null): number {
|
||||
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
||||
const state = this.guildStates.get(id as GuildId);
|
||||
return state?.mentionCount.get() ?? 0;
|
||||
}
|
||||
|
||||
getTotalMentionCount(excludePrivate = false): number {
|
||||
mentionCountAcrossGuilds(excludePrivate = false): number {
|
||||
let total = 0;
|
||||
for (const [guildId, state] of this.guildStates.entries()) {
|
||||
if (excludePrivate && guildId === PRIVATE_CHANNEL_SENTINEL) continue;
|
||||
@@ -358,22 +356,22 @@ class GuildReadState {
|
||||
return total;
|
||||
}
|
||||
|
||||
getPrivateChannelMentionCount(): number {
|
||||
directMessageMentionCount(): number {
|
||||
const state = this.guildStates.get(PRIVATE_CHANNEL_SENTINEL as GuildId);
|
||||
return state?.mentionCount.get() ?? 0;
|
||||
}
|
||||
|
||||
getMentionCountForPrivateChannel(channelId: string): number {
|
||||
directMessageMentionCountFor(channelId: string): number {
|
||||
return ReadStates.getMentionCount(channelId);
|
||||
}
|
||||
|
||||
getGuildChangeSentinel(guildId: string | null): number {
|
||||
guildChangeTicketFor(guildId: string | null): number {
|
||||
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
||||
const state = this.guildStates.get(id as GuildId);
|
||||
return state?.sentinel.get() ?? 0;
|
||||
return state?.changeTicket.get() ?? 0;
|
||||
}
|
||||
|
||||
getGuildHasUnreadIgnoreMuted(guildId: string): boolean {
|
||||
guildUnreadIgnoringMute(guildId: string): boolean {
|
||||
const channels = Channels.getGuildChannels(guildId);
|
||||
for (const channel of channels) {
|
||||
if (!GUILD_TEXT_BASED_CHANNEL_TYPES.has(channel.type)) {
|
||||
@@ -388,7 +386,7 @@ class GuildReadState {
|
||||
|
||||
handleConnectionOpen(): void {
|
||||
this.guildStates.clear();
|
||||
this.unreadGuilds.clear();
|
||||
this.unreadGuildIds.clear();
|
||||
this.updateCounter = 0;
|
||||
this.recomputeAll(null);
|
||||
for (const guildId of Guilds.getGuildIds()) {
|
||||
@@ -411,7 +409,7 @@ class GuildReadState {
|
||||
};
|
||||
}): void {
|
||||
this.guildStates.delete(action.guild.id as GuildId);
|
||||
this.unreadGuilds.delete(action.guild.id as GuildId);
|
||||
this.unreadGuildIds.delete(action.guild.id as GuildId);
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
@@ -459,7 +457,7 @@ class GuildReadState {
|
||||
this.handleConnectionOpen();
|
||||
}
|
||||
|
||||
handleWindowFocus(): void {
|
||||
handleWindowFocused(): void {
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
|
||||
@@ -553,12 +553,12 @@ export class Message {
|
||||
export const messageMentionsCurrentUser = (message: WireMessage): boolean => {
|
||||
const channel = Channels.getChannel(message.channel_id);
|
||||
if (!channel) return false;
|
||||
if (message.mention_everyone && !UserGuildSettings.isSuppressEveryoneEnabled(channel.guildId ?? null)) return true;
|
||||
if (message.mention_everyone && !UserGuildSettings.isEveryoneMentionSuppressed(channel.guildId ?? null)) return true;
|
||||
if (message.mentions?.some((user) => user.id === Authentication.currentUserId)) {
|
||||
return true;
|
||||
}
|
||||
if (!channel.guildId) return false;
|
||||
if (UserGuildSettings.isSuppressRolesEnabled(channel.guildId)) return false;
|
||||
if (UserGuildSettings.isRoleMentionSuppressed(channel.guildId)) return false;
|
||||
const guild = Guilds.getGuild(channel.guildId);
|
||||
if (!guild) return false;
|
||||
const guildMember = GuildMembers.getMember(guild.id, Authentication.currentUserId);
|
||||
|
||||
@@ -16,7 +16,7 @@ export function isMentioned(user: User, message: Message): boolean {
|
||||
logger.warn(`${message.channelId} does not exist!`);
|
||||
return false;
|
||||
}
|
||||
const suppressEveryone = UserGuildSettings.isSuppressEveryoneEnabled(channel.guildId ?? null);
|
||||
const suppressEveryone = UserGuildSettings.isEveryoneMentionSuppressed(channel.guildId ?? null);
|
||||
const mentionEveryone = message.mentionEveryone && !suppressEveryone;
|
||||
if (mentionEveryone) {
|
||||
return true;
|
||||
@@ -35,6 +35,6 @@ export function isMentioned(user: User, message: Message): boolean {
|
||||
if (!guildMember) {
|
||||
return false;
|
||||
}
|
||||
const suppressRoles = UserGuildSettings.isSuppressRolesEnabled(channel.guildId);
|
||||
const suppressRoles = UserGuildSettings.isRoleMentionSuppressed(channel.guildId);
|
||||
return !suppressRoles && message.mentionRoles.some((roleId) => guildMember.roles.has(roleId));
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export function useMuteSheet(params: UseMuteSheetParams): UseMuteSheetReturn {
|
||||
const additionalMutePayload = isGuildMode ? undefined : params.additionalMutePayload;
|
||||
const {onMuteSuccess, onUnmuteSuccess, onClose} = params;
|
||||
const muteConfig = isGuildMode
|
||||
? UserGuildSettings.getSettings(guildId)?.mute_config
|
||||
? UserGuildSettings.getSettingsForScope(guildId)?.mute_config
|
||||
: UserGuildSettings.getChannelOverride(guildId, channelId!)?.mute_config;
|
||||
const openMuteSheet = useCallback(() => {
|
||||
setMuteSheetOpen(true);
|
||||
|
||||
@@ -231,8 +231,8 @@ export class ReadStateEntry {
|
||||
const userId = currentUser.id;
|
||||
const guildId = this.guildId;
|
||||
const channelId = this.channelId;
|
||||
const suppressEveryone = recomputeMentions ? UserGuildSettings.isSuppressEveryoneEnabled(guildId) : false;
|
||||
const suppressRoles = recomputeMentions ? UserGuildSettings.isSuppressRolesEnabled(guildId) : false;
|
||||
const suppressEveryone = recomputeMentions ? UserGuildSettings.isEveryoneMentionSuppressed(guildId) : false;
|
||||
const suppressRoles = recomputeMentions ? UserGuildSettings.isRoleMentionSuppressed(guildId) : false;
|
||||
const isMuted = recomputeMentions ? UserGuildSettings.isGuildOrChannelMuted(guildId, channelId) : false;
|
||||
const member = recomputeMentions && guildId ? GuildMembers.getMember(guildId, userId) : null;
|
||||
const memberRoles = member?.roles ?? null;
|
||||
@@ -285,8 +285,8 @@ export class ReadStateEntry {
|
||||
|
||||
shouldMentionFor(message: MessageModel | WireMessage, userId: string, isPrivate: boolean): boolean {
|
||||
const authorBlocked = Relationships.isBlocked(message.author.id);
|
||||
const suppressEveryone = UserGuildSettings.isSuppressEveryoneEnabled(this.guildId);
|
||||
const suppressRoles = UserGuildSettings.isSuppressRolesEnabled(this.guildId);
|
||||
const suppressEveryone = UserGuildSettings.isEveryoneMentionSuppressed(this.guildId);
|
||||
const suppressRoles = UserGuildSettings.isRoleMentionSuppressed(this.guildId);
|
||||
const mentions = message.mentions;
|
||||
const mentionEveryone = 'mentionEveryone' in message ? message.mentionEveryone : message.mention_everyone;
|
||||
const mentionRoles = 'mentionRoles' in message ? message.mentionRoles : message.mention_roles;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const ChannelListContextMenu: React.FC<ChannelListContextMenuProps> = obs
|
||||
});
|
||||
const invitableChannelId = InviteUtils.getInvitableChannelId(guild.id);
|
||||
const canInvite = InviteUtils.canInviteToChannel(invitableChannelId, guild.id);
|
||||
const hideMutedChannels = UserGuildSettings.getSettings(guild.id)?.hide_muted_channels ?? false;
|
||||
const hideMutedChannels = UserGuildSettings.getSettingsForScope(guild.id)?.hide_muted_channels ?? false;
|
||||
const handleToggleHideMutedChannels = useCallback(() => {
|
||||
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
||||
}, [guild.id]);
|
||||
|
||||
@@ -22,7 +22,7 @@ interface FavoritesGuildContextMenuProps {
|
||||
|
||||
export const FavoritesGuildContextMenu: React.FC<FavoritesGuildContextMenuProps> = observer(({onClose}) => {
|
||||
const {i18n} = useLingui();
|
||||
const settings = UserGuildSettings.getSettings(FAVORITES_GUILD_ID);
|
||||
const settings = UserGuildSettings.getSettingsForScope(FAVORITES_GUILD_ID);
|
||||
const isMuted = settings?.muted ?? false;
|
||||
const handleToggleMute = () => {
|
||||
UserGuildSettingsCommands.updateGuildSettings(FAVORITES_GUILD_ID, {muted: !isMuted});
|
||||
|
||||
@@ -34,7 +34,7 @@ interface GuildFolderContextMenuProps {
|
||||
}
|
||||
|
||||
function guildHasUnreadState(guildId: string): boolean {
|
||||
return GuildReadState.hasUnread(guildId) || GuildReadState.getGuildHasUnreadIgnoreMuted(guildId);
|
||||
return GuildReadState.guildIsUnread(guildId) || GuildReadState.guildUnreadIgnoringMute(guildId);
|
||||
}
|
||||
|
||||
export const GuildFolderContextMenu: React.FC<GuildFolderContextMenuProps> = observer(({folder, guilds, onClose}) => {
|
||||
|
||||
@@ -132,11 +132,11 @@ function getCategoryMenuState(category: Channel): CategoryMenuState {
|
||||
(ch) => ch.parentId === category.id && ch.type !== ChannelTypes.GUILD_CATEGORY,
|
||||
);
|
||||
const hasUnread = channelsInCategory.some((ch) => ReadStates.hasUnread(ch.id));
|
||||
const isCollapsed = UserGuildSettings.isChannelCollapsed(guildId, category.id);
|
||||
const isCollapsed = UserGuildSettings.isChannelSectionCollapsed(guildId, category.id);
|
||||
const categoryIds = channels.filter((ch) => ch.type === ChannelTypes.GUILD_CATEGORY).map((ch) => ch.id);
|
||||
const allCategoriesCollapsed =
|
||||
categoryIds.length > 0
|
||||
? categoryIds.every((categoryId) => UserGuildSettings.isChannelCollapsed(guildId, categoryId))
|
||||
? categoryIds.every((categoryId) => UserGuildSettings.isChannelSectionCollapsed(guildId, categoryId))
|
||||
: false;
|
||||
const categoryOverride = UserGuildSettings.getChannelOverride(guildId, category.id);
|
||||
const isMuted = categoryOverride?.muted ?? false;
|
||||
|
||||
@@ -113,7 +113,7 @@ export const MarkCategoryAsReadMenuItem: React.FC<CategoryMenuItemProps> = obser
|
||||
export const CollapseCategoryMenuItem: React.FC<CategoryMenuItemProps> = observer(({category, onClose}) => {
|
||||
const {i18n} = useLingui();
|
||||
const guildId = category.guildId!;
|
||||
const isCollapsed = UserGuildSettings.isChannelCollapsed(guildId, category.id);
|
||||
const isCollapsed = UserGuildSettings.isChannelSectionCollapsed(guildId, category.id);
|
||||
const handleToggleCollapse = useCallback(() => {
|
||||
UserGuildSettingsCommands.toggleChannelCollapsed(guildId, category.id);
|
||||
onClose();
|
||||
@@ -138,7 +138,7 @@ export const CollapseAllCategoriesMenuItem: React.FC<CategoryMenuItemProps> = ob
|
||||
);
|
||||
const allCategoriesCollapsed = useMemo(() => {
|
||||
if (categoryIds.length === 0) return false;
|
||||
return categoryIds.every((categoryId) => UserGuildSettings.isChannelCollapsed(guildId, categoryId));
|
||||
return categoryIds.every((categoryId) => UserGuildSettings.isChannelSectionCollapsed(guildId, categoryId));
|
||||
}, [guildId, categoryIds]);
|
||||
const handleToggleCollapseAll = useCallback(() => {
|
||||
UserGuildSettingsCommands.toggleAllCategoriesCollapsed(guildId, categoryIds);
|
||||
|
||||
@@ -164,7 +164,7 @@ export function useGuildMenuData(guild: Guild, options: UseGuildMenuDataOptions)
|
||||
const isOwner = guild.isOwner(Authentication.currentUserId);
|
||||
const developerMode = UserSettings.developerMode;
|
||||
const canEditCommunityProfile = Users.getCurrentUser()?.isClaimed() ?? true;
|
||||
const settings = UserGuildSettings.getSettings(guild.id);
|
||||
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const hideMutedChannels = settings?.hide_muted_channels ?? false;
|
||||
const isMuted = settings?.muted ?? false;
|
||||
const muteConfig = settings?.mute_config;
|
||||
@@ -298,7 +298,7 @@ export function useGuildMenuData(guild: Guild, options: UseGuildMenuDataOptions)
|
||||
ModalCommands.runAfterBottomSheetClose(onClose, () => openReportGuildModal({i18n, guild}));
|
||||
},
|
||||
handleToggleHideMutedChannels: (checked: boolean) => {
|
||||
const currentSettings = UserGuildSettings.getSettings(guild.id);
|
||||
const currentSettings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
||||
if (checked === currentValue) return;
|
||||
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
||||
|
||||
@@ -148,7 +148,7 @@ export const InvitePeopleMenuItem: React.FC<GuildMenuItemProps> = observer(({gui
|
||||
});
|
||||
export const MuteCommunityMenuItem: React.FC<GuildMenuItemProps> = observer(({guild, onClose}) => {
|
||||
const {i18n} = useLingui();
|
||||
const settings = UserGuildSettings.getSettings(guild.id);
|
||||
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const isMuted = settings?.muted ?? false;
|
||||
const muteConfig = settings?.mute_config;
|
||||
const mutedText = getMutedText(isMuted, muteConfig);
|
||||
@@ -219,7 +219,7 @@ export const MuteCommunityMenuItem: React.FC<GuildMenuItemProps> = observer(({gu
|
||||
});
|
||||
export const NotificationSettingsMenuItem: React.FC<GuildMenuItemProps> = observer(({guild, onClose}) => {
|
||||
const {i18n} = useLingui();
|
||||
const settings = UserGuildSettings.getSettings(guild.id);
|
||||
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const suppressEveryone = settings?.suppress_everyone ?? false;
|
||||
const suppressRoles = settings?.suppress_roles ?? false;
|
||||
const mobilePush = settings?.mobile_push ?? true;
|
||||
@@ -323,11 +323,11 @@ export const NotificationSettingsMenuItem: React.FC<GuildMenuItemProps> = observ
|
||||
});
|
||||
export const HideMutedChannelsMenuItem: React.FC<GuildMenuItemProps> = observer(({guild}) => {
|
||||
const {i18n} = useLingui();
|
||||
const settings = UserGuildSettings.getSettings(guild.id);
|
||||
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const hideMutedChannels = settings?.hide_muted_channels ?? false;
|
||||
const handleToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
const currentSettings = UserGuildSettings.getSettings(guild.id);
|
||||
const currentSettings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
||||
if (checked === currentValue) return;
|
||||
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
||||
|
||||
@@ -223,7 +223,7 @@ class NotificationState {
|
||||
}
|
||||
|
||||
private shouldNotifyBasedOnSettings(channel: Channel, messageRecord: Message, currentUser: User): boolean {
|
||||
const level = UserGuildSettings.resolvedMessageNotifications({
|
||||
const level = UserGuildSettings.resolveEffectiveMessageNotifications({
|
||||
id: channel.id,
|
||||
guildId: channel.guildId,
|
||||
parentId: channel.parentId ?? undefined,
|
||||
@@ -261,7 +261,7 @@ class NotificationState {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
UserGuildSettings.allowNoMessages({
|
||||
UserGuildSettings.resolvesToNoMessages({
|
||||
id: channel.id,
|
||||
guildId: channel.guildId,
|
||||
parentId: channel.parentId ?? undefined,
|
||||
|
||||
@@ -127,7 +127,7 @@ export function updateGuildSettings(
|
||||
updates: UserGuildSettingsPartial,
|
||||
options?: PersistenceOptions,
|
||||
): void {
|
||||
UserGuildSettings.getSettings(guildId);
|
||||
UserGuildSettings.getSettingsForScope(guildId);
|
||||
UserGuildSettings.updateGuildSettings(guildId, updates as Partial<GatewayGuildSettings>);
|
||||
scheduleUpdate(guildId, updates, options);
|
||||
}
|
||||
@@ -139,7 +139,7 @@ export function bulkUpdateGuildSettings(
|
||||
const uniqueGuildIds = Array.from(new Set(guildIds));
|
||||
if (uniqueGuildIds.length === 0) return;
|
||||
for (const guildId of uniqueGuildIds) {
|
||||
UserGuildSettings.getSettings(guildId);
|
||||
UserGuildSettings.getSettingsForScope(guildId);
|
||||
UserGuildSettings.updateGuildSettings(guildId, updates as Partial<GatewayGuildSettings>);
|
||||
const key = getUpdateKey(guildId);
|
||||
addPendingPayload(key, updates);
|
||||
@@ -148,7 +148,7 @@ export function bulkUpdateGuildSettings(
|
||||
}
|
||||
|
||||
export function toggleHideMutedChannels(guildId: string | null): void {
|
||||
const currentSettings = UserGuildSettings.getSettings(guildId);
|
||||
const currentSettings = UserGuildSettings.getSettingsForScope(guildId);
|
||||
const newValue = !currentSettings.hide_muted_channels;
|
||||
updateGuildSettings(guildId, {hide_muted_channels: newValue});
|
||||
}
|
||||
@@ -159,7 +159,7 @@ export function updateChannelOverride(
|
||||
override: ChannelOverrideUpdate,
|
||||
options?: PersistenceOptions,
|
||||
): void {
|
||||
const currentSettings = UserGuildSettings.getSettings(guildId);
|
||||
const currentSettings = UserGuildSettings.getSettingsForScope(guildId);
|
||||
const currentOverride = UserGuildSettings.getChannelOverride(guildId, channelId);
|
||||
let newOverride: ChannelOverride | null = null;
|
||||
if (override != null) {
|
||||
@@ -182,7 +182,7 @@ export function bulkUpdateChannelOverrides(
|
||||
): void {
|
||||
const uniqueChannelIds = Array.from(new Set(channelIds));
|
||||
if (uniqueChannelIds.length === 0) return;
|
||||
const currentSettings = UserGuildSettings.getSettings(guildId);
|
||||
const currentSettings = UserGuildSettings.getSettingsForScope(guildId);
|
||||
const newChannelOverrides: Record<string, ChannelOverride> = {...(currentSettings.channel_overrides ?? {})};
|
||||
for (const channelId of uniqueChannelIds) {
|
||||
const currentOverride = UserGuildSettings.getChannelOverride(guildId, channelId);
|
||||
@@ -196,7 +196,7 @@ export function bulkUpdateChannelOverrides(
|
||||
}
|
||||
|
||||
export function toggleChannelCollapsed(guildId: string | null, channelId: string): void {
|
||||
const isCollapsed = UserGuildSettings.isChannelCollapsed(guildId, channelId);
|
||||
const isCollapsed = UserGuildSettings.isChannelSectionCollapsed(guildId, channelId);
|
||||
updateChannelOverride(guildId, channelId, {collapsed: !isCollapsed});
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ export function updateUnreadBadgesLevel(
|
||||
}
|
||||
|
||||
export function toggleChannelMuted(guildId: string | null, channelId: string, options?: PersistenceOptions): void {
|
||||
const isMuted = UserGuildSettings.isChannelMuted(guildId, channelId);
|
||||
const isMuted = UserGuildSettings.isChannelDirectlyMuted(guildId, channelId);
|
||||
updateChannelOverride(guildId, channelId, {muted: !isMuted}, options);
|
||||
}
|
||||
|
||||
@@ -236,13 +236,13 @@ export function toggleAllCategoriesCollapsed(guildId: string | null, categoryIds
|
||||
const uniqueCategoryIds = Array.from(new Set(categoryIds));
|
||||
if (uniqueCategoryIds.length === 0) return;
|
||||
const allCollapsed = uniqueCategoryIds.every((categoryId) =>
|
||||
UserGuildSettings.isChannelCollapsed(guildId, categoryId),
|
||||
UserGuildSettings.isChannelSectionCollapsed(guildId, categoryId),
|
||||
);
|
||||
const newCollapsedState = !allCollapsed;
|
||||
for (const categoryId of uniqueCategoryIds) {
|
||||
UserGuildSettings.updateChannelOverride(guildId, categoryId, {collapsed: newCollapsedState});
|
||||
}
|
||||
const currentSettings = UserGuildSettings.getSettings(guildId);
|
||||
const currentSettings = UserGuildSettings.getSettingsForScope(guildId);
|
||||
const newChannelOverrides: Record<string, ChannelOverride> = {...(currentSettings.channel_overrides ?? {})};
|
||||
for (const categoryId of uniqueCategoryIds) {
|
||||
const currentOverride = newChannelOverrides[categoryId];
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface ChannelOverride {
|
||||
unread_badges?: number | null;
|
||||
}
|
||||
|
||||
interface GuildSettings {
|
||||
interface StoredGuildSettings {
|
||||
guild_id: string;
|
||||
suppress_everyone: boolean;
|
||||
suppress_roles: boolean;
|
||||
@@ -63,7 +63,7 @@ export interface GatewayGuildSettings {
|
||||
unread_badges?: number | null;
|
||||
}
|
||||
|
||||
const DEFAULT_GUILD_SETTINGS: Omit<GuildSettings, 'guild_id'> = {
|
||||
const DEFAULT_GUILD_SETTINGS: Omit<StoredGuildSettings, 'guild_id'> = {
|
||||
suppress_everyone: false,
|
||||
suppress_roles: false,
|
||||
mute_scheduled_events: false,
|
||||
@@ -95,7 +95,7 @@ function parseEndTime(end_time?: string): number | null {
|
||||
return Number.isFinite(ms) ? ms : null;
|
||||
}
|
||||
|
||||
function isMuted(config: {
|
||||
function isMuteActive(config: {
|
||||
muted?: boolean;
|
||||
mute_config?: {
|
||||
end_time?: string;
|
||||
@@ -114,8 +114,8 @@ interface UnreadInboxReadState {
|
||||
}
|
||||
|
||||
class UserGuildSettings {
|
||||
private readonly guildSettings = observable.map<GuildId, GuildSettings>();
|
||||
private readonly mutedChannels = observable.map<GuildId, Set<ChannelId>>();
|
||||
private readonly guildSettings = observable.map<GuildId, StoredGuildSettings>();
|
||||
private readonly mutedChannelsByGuild = observable.map<GuildId, Set<ChannelId>>();
|
||||
updateCounter = 0;
|
||||
private readonly pendingGuildUpdates = new Set<GuildId | null>();
|
||||
private readonly guildMuteTimers = new Map<GuildId, NodeJS.Timeout>();
|
||||
@@ -149,7 +149,7 @@ class UserGuildSettings {
|
||||
this.pendingGuildUpdates.add(guildId as GuildId | null);
|
||||
}
|
||||
|
||||
private getDefaultSettings(guildId: string): GuildSettings {
|
||||
private getDefaultSettings(guildId: string): StoredGuildSettings {
|
||||
return {
|
||||
...DEFAULT_GUILD_SETTINGS,
|
||||
guild_id: guildId,
|
||||
@@ -159,12 +159,12 @@ class UserGuildSettings {
|
||||
};
|
||||
}
|
||||
|
||||
private getStoredSettings(guildId: string | null): GuildSettings | undefined {
|
||||
private getStoredSettings(guildId: string | null): StoredGuildSettings | undefined {
|
||||
const key = this.storageKeyFor(guildId);
|
||||
return this.guildSettings.get(key as GuildId);
|
||||
}
|
||||
|
||||
private ensureGuildSettings(guildId: string | null): GuildSettings {
|
||||
private ensureGuildSettings(guildId: string | null): StoredGuildSettings {
|
||||
const key = this.storageKeyFor(guildId);
|
||||
let settings = this.getStoredSettings(key);
|
||||
if (settings == null) {
|
||||
@@ -174,12 +174,12 @@ class UserGuildSettings {
|
||||
return settings;
|
||||
}
|
||||
|
||||
getGuildSettings(guildId: string | null): GuildSettings {
|
||||
getGuildSettings(guildId: string | null): StoredGuildSettings {
|
||||
const key = this.storageKeyFor(guildId);
|
||||
return this.getStoredSettings(key) ?? this.getDefaultSettings(key);
|
||||
}
|
||||
|
||||
getSettings(guildId: string | null): GuildSettings {
|
||||
getSettingsForScope(guildId: string | null): StoredGuildSettings {
|
||||
return this.getGuildSettings(guildId);
|
||||
}
|
||||
|
||||
@@ -208,17 +208,17 @@ class UserGuildSettings {
|
||||
return result as Record<ChannelId, ChannelOverride>;
|
||||
}
|
||||
|
||||
private updateMutedChannelsCache(guildId: string, settings: GuildSettings): void {
|
||||
private updateMutedChannelsCache(guildId: string, settings: StoredGuildSettings): void {
|
||||
const set = new Set<ChannelId>();
|
||||
for (const [channelId, override] of Object.entries(settings.channel_overrides)) {
|
||||
if (isMuted(override)) {
|
||||
if (isMuteActive(override)) {
|
||||
set.add(channelId as ChannelId);
|
||||
}
|
||||
}
|
||||
if (set.size > 0) {
|
||||
this.mutedChannels.set(guildId as GuildId, set);
|
||||
this.mutedChannelsByGuild.set(guildId as GuildId, set);
|
||||
} else {
|
||||
this.mutedChannels.delete(guildId as GuildId);
|
||||
this.mutedChannelsByGuild.delete(guildId as GuildId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ class UserGuildSettings {
|
||||
}
|
||||
}
|
||||
|
||||
private setupMuteTimers(guildId: string, settings: GuildSettings): void {
|
||||
private setupMuteTimers(guildId: string, settings: StoredGuildSettings): void {
|
||||
this.clearGuildMuteTimer(guildId);
|
||||
this.clearGuildChannelTimers(guildId);
|
||||
if (settings.muted && settings.mute_config?.end_time) {
|
||||
@@ -287,7 +287,7 @@ class UserGuildSettings {
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeGuildMute(settings: GuildSettings): GuildSettings {
|
||||
private sanitizeGuildMute(settings: StoredGuildSettings): StoredGuildSettings {
|
||||
if (!settings.muted) return settings;
|
||||
const endMs = parseEndTime(settings.mute_config?.end_time);
|
||||
if (endMs != null && endMs <= Date.now()) {
|
||||
@@ -296,7 +296,7 @@ class UserGuildSettings {
|
||||
return settings;
|
||||
}
|
||||
|
||||
private sanitizeChannelMutes(settings: GuildSettings): GuildSettings {
|
||||
private sanitizeChannelMutes(settings: StoredGuildSettings): StoredGuildSettings {
|
||||
let changed = false;
|
||||
const overrides: Record<string, ChannelOverride> = {...settings.channel_overrides};
|
||||
for (const [channelId, override] of Object.entries(overrides)) {
|
||||
@@ -314,7 +314,7 @@ class UserGuildSettings {
|
||||
const key = this.storageKeyFor(guildId);
|
||||
const existing = this.ensureGuildSettings(key);
|
||||
const overridesInput = 'channel_overrides' in updates ? updates.channel_overrides : existing.channel_overrides;
|
||||
const merged: GuildSettings = {
|
||||
const merged: StoredGuildSettings = {
|
||||
...existing,
|
||||
...updates,
|
||||
guild_id: key,
|
||||
@@ -333,7 +333,7 @@ class UserGuildSettings {
|
||||
const settings = this.ensureGuildSettings(key);
|
||||
const existing = settings.channel_overrides[channelId as ChannelId] ?? DEFAULT_CHANNEL_OVERRIDE(channelId);
|
||||
const updated = {...existing, ...updates, channel_id: channelId};
|
||||
const next: GuildSettings = {
|
||||
const next: StoredGuildSettings = {
|
||||
...settings,
|
||||
channel_overrides: {...settings.channel_overrides, [channelId]: updated} as Record<ChannelId, ChannelOverride>,
|
||||
};
|
||||
@@ -353,7 +353,10 @@ class UserGuildSettings {
|
||||
const existing = nextOverrides[channelId] ?? DEFAULT_CHANNEL_OVERRIDE(channelId);
|
||||
nextOverrides[channelId] = {...existing, ...updates, channel_id: channelId};
|
||||
}
|
||||
const next: GuildSettings = {...settings, channel_overrides: nextOverrides as Record<ChannelId, ChannelOverride>};
|
||||
const next: StoredGuildSettings = {
|
||||
...settings,
|
||||
channel_overrides: nextOverrides as Record<ChannelId, ChannelOverride>,
|
||||
};
|
||||
const sanitized = this.sanitizeChannelMutes(this.sanitizeGuildMute(next));
|
||||
this.guildSettings.set(key as GuildId, sanitized);
|
||||
this.updateMutedChannelsCache(key, sanitized);
|
||||
@@ -362,24 +365,24 @@ class UserGuildSettings {
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
isSuppressEveryoneEnabled(guildId: string | null): boolean {
|
||||
isEveryoneMentionSuppressed(guildId: string | null): boolean {
|
||||
return guildId != null && this.getGuildSettings(guildId).suppress_everyone;
|
||||
}
|
||||
|
||||
isSuppressRolesEnabled(guildId: string | null): boolean {
|
||||
isRoleMentionSuppressed(guildId: string | null): boolean {
|
||||
return guildId != null && this.getGuildSettings(guildId).suppress_roles;
|
||||
}
|
||||
|
||||
isMuteScheduledEventsEnabled(guildId: string | null): boolean {
|
||||
areScheduledEventsMuted(guildId: string | null): boolean {
|
||||
return guildId != null && this.getGuildSettings(guildId).mute_scheduled_events;
|
||||
}
|
||||
|
||||
isMobilePushEnabled(guildId: string | null): boolean {
|
||||
isMobilePushAllowed(guildId: string | null): boolean {
|
||||
return guildId == null || this.getGuildSettings(guildId).mobile_push;
|
||||
}
|
||||
|
||||
isMuted(guildId: string | null): boolean {
|
||||
return isMuted(this.getGuildSettings(guildId));
|
||||
isWholeGuildMuted(guildId: string | null): boolean {
|
||||
return isMuteActive(this.getGuildSettings(guildId));
|
||||
}
|
||||
|
||||
getGuildMessageNotifications(guildId: string | null): number {
|
||||
@@ -401,7 +404,7 @@ class UserGuildSettings {
|
||||
return settings.message_notifications;
|
||||
}
|
||||
|
||||
getGuildIds(): Array<GuildId> {
|
||||
getKnownGuildIds(): Array<GuildId> {
|
||||
return Array.from(this.guildSettings.keys());
|
||||
}
|
||||
|
||||
@@ -409,7 +412,7 @@ class UserGuildSettings {
|
||||
return this.getGuildSettings(guildId).message_notifications;
|
||||
}
|
||||
|
||||
getChannelOverrides(guildId: string | null): Record<ChannelId, ChannelOverride> {
|
||||
getAllChannelOverrides(guildId: string | null): Record<ChannelId, ChannelOverride> {
|
||||
return this.getGuildSettings(guildId).channel_overrides;
|
||||
}
|
||||
|
||||
@@ -417,50 +420,59 @@ class UserGuildSettings {
|
||||
return this.getGuildSettings(guildId).channel_overrides[channelId as ChannelId];
|
||||
}
|
||||
|
||||
getChannelMessageNotifications(guildId: string | null, channelId: string): number {
|
||||
getChannelNotificationLevel(guildId: string | null, channelId: string): number {
|
||||
return (
|
||||
this.getGuildSettings(guildId).channel_overrides[channelId as ChannelId]?.message_notifications ??
|
||||
MessageNotifications.NULL
|
||||
);
|
||||
}
|
||||
|
||||
isChannelMuted(guildId: string | null, channelId: string): boolean {
|
||||
isChannelDirectlyMuted(guildId: string | null, channelId: string): boolean {
|
||||
const override = this.getGuildSettings(guildId).channel_overrides[channelId as ChannelId];
|
||||
return override != null && isMuted(override);
|
||||
return override != null && isMuteActive(override);
|
||||
}
|
||||
|
||||
isCategoryMuted(guildId: string | null, channelId: string): boolean {
|
||||
isParentCategoryMuted(guildId: string | null, channelId: string): boolean {
|
||||
if (guildId == null) return false;
|
||||
const channel = Channels.getChannel(channelId);
|
||||
return channel?.parentId != null && this.isChannelMuted(guildId, channel.parentId);
|
||||
return channel?.parentId != null && this.isChannelDirectlyMuted(guildId, channel.parentId);
|
||||
}
|
||||
|
||||
isGuildOrCategoryOrChannelMuted(guildId: string | null, channelId: string): boolean {
|
||||
return this.isMuted(guildId) || this.isCategoryMuted(guildId, channelId) || this.isChannelMuted(guildId, channelId);
|
||||
isMutedAtAnyLevel(guildId: string | null, channelId: string): boolean {
|
||||
return (
|
||||
this.isWholeGuildMuted(guildId) ||
|
||||
this.isParentCategoryMuted(guildId, channelId) ||
|
||||
this.isChannelDirectlyMuted(guildId, channelId)
|
||||
);
|
||||
}
|
||||
|
||||
isGuildOrChannelMuted(guildId: string | null, channelId: string): boolean {
|
||||
return this.isMuted(guildId) || this.isChannelMuted(guildId, channelId);
|
||||
return this.isWholeGuildMuted(guildId) || this.isChannelDirectlyMuted(guildId, channelId);
|
||||
}
|
||||
|
||||
isCategoryOrChannelMuted(guildId: string | null, channelId: string): boolean {
|
||||
return this.isCategoryMuted(guildId, channelId) || this.isChannelMuted(guildId, channelId);
|
||||
return this.isParentCategoryMuted(guildId, channelId) || this.isChannelDirectlyMuted(guildId, channelId);
|
||||
}
|
||||
|
||||
getMutedChannels(guildId: string): Set<ChannelId> {
|
||||
return new Set(this.mutedChannels.get(guildId as GuildId) ?? []);
|
||||
getMutedChannelIds(guildId: string): Set<ChannelId> {
|
||||
return new Set(this.mutedChannelsByGuild.get(guildId as GuildId) ?? []);
|
||||
}
|
||||
|
||||
isChannelCollapsed(guildId: string | null, channelId: string): boolean {
|
||||
isChannelSectionCollapsed(guildId: string | null, channelId: string): boolean {
|
||||
return this.getChannelOverride(guildId, channelId)?.collapsed ?? false;
|
||||
}
|
||||
|
||||
resolvedMessageNotifications(channel: {id: string; guildId?: string; parentId?: string; type: number}): number {
|
||||
resolveEffectiveMessageNotifications(channel: {
|
||||
id: string;
|
||||
guildId?: string;
|
||||
parentId?: string;
|
||||
type: number;
|
||||
}): number {
|
||||
const guildId = channel.guildId ?? null;
|
||||
const direct = this.getChannelMessageNotifications(guildId, channel.id);
|
||||
const direct = this.getChannelNotificationLevel(guildId, channel.id);
|
||||
if (direct !== MessageNotifications.NULL && direct !== MessageNotifications.INHERIT) return direct;
|
||||
if (channel.parentId != null && guildId != null) {
|
||||
const parent = this.getChannelMessageNotifications(guildId, channel.parentId);
|
||||
const parent = this.getChannelNotificationLevel(guildId, channel.parentId);
|
||||
if (parent !== MessageNotifications.NULL && parent !== MessageNotifications.INHERIT) return parent;
|
||||
}
|
||||
return this.getGuildMessageNotifications(guildId);
|
||||
@@ -480,7 +492,7 @@ class UserGuildSettings {
|
||||
|
||||
private getLockedCommunityUnreadBadgesLevel(guildId: string | null): number | null {
|
||||
if (guildId == null) return null;
|
||||
if (this.isMuted(guildId)) return MessageNotifications.NO_MESSAGES;
|
||||
if (this.isWholeGuildMuted(guildId)) return MessageNotifications.NO_MESSAGES;
|
||||
const notificationLevel = this.getGuildMessageNotifications(guildId);
|
||||
if (notificationLevel === MessageNotifications.ALL_MESSAGES) return MessageNotifications.ALL_MESSAGES;
|
||||
if (notificationLevel === MessageNotifications.NO_MESSAGES) return MessageNotifications.NO_MESSAGES;
|
||||
@@ -527,35 +539,35 @@ class UserGuildSettings {
|
||||
const guildId = channel.guildId ?? null;
|
||||
return resolveUnreadInboxVisibility({
|
||||
unreadBadgesLevel: this.resolvedUnreadBadgesLevel(channel),
|
||||
isMuted: this.isGuildOrCategoryOrChannelMuted(guildId, channel.id),
|
||||
isMuted: this.isMutedAtAnyLevel(guildId, channel.id),
|
||||
hasUnread: readState.hasUnread,
|
||||
hasMentions: readState.hasMentions,
|
||||
});
|
||||
}
|
||||
|
||||
resolveUnreadSetting(channel: {id: string; guildId?: string; parentId?: string; type: number}): string {
|
||||
const level = this.resolvedMessageNotifications(channel);
|
||||
getUnreadSettingKey(channel: {id: string; guildId?: string; parentId?: string; type: number}): string {
|
||||
const level = this.resolveEffectiveMessageNotifications(channel);
|
||||
return level === MessageNotifications.ALL_MESSAGES ? 'all_messages' : 'only_mentions';
|
||||
}
|
||||
|
||||
allowNoMessages(channel: {id: string; guildId?: string; parentId?: string; type: number}): boolean {
|
||||
resolvesToNoMessages(channel: {id: string; guildId?: string; parentId?: string; type: number}): boolean {
|
||||
return (
|
||||
this.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) ||
|
||||
this.resolvedMessageNotifications(channel) === MessageNotifications.NO_MESSAGES
|
||||
this.resolveEffectiveMessageNotifications(channel) === MessageNotifications.NO_MESSAGES
|
||||
);
|
||||
}
|
||||
|
||||
allowAllMessages(channel: {id: string; guildId?: string; parentId?: string; type: number}): boolean {
|
||||
resolvesToAllMessages(channel: {id: string; guildId?: string; parentId?: string; type: number}): boolean {
|
||||
return (
|
||||
!this.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) &&
|
||||
this.resolvedMessageNotifications(channel) === MessageNotifications.ALL_MESSAGES
|
||||
this.resolveEffectiveMessageNotifications(channel) === MessageNotifications.ALL_MESSAGES
|
||||
);
|
||||
}
|
||||
|
||||
handleConnectionOpen(userGuildSettings: Array<GatewayGuildSettings>): void {
|
||||
this.clearAllTimers();
|
||||
this.guildSettings.clear();
|
||||
this.mutedChannels.clear();
|
||||
this.mutedChannelsByGuild.clear();
|
||||
for (const settings of userGuildSettings) {
|
||||
this.updateGuildSettings(settings.guild_id, settings);
|
||||
}
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/app/state/RuntimeConfig', () => ({
|
||||
default: {staticCdnEndpoint: 'https://cdn.test'},
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/voice/state/VoiceSettings', () => ({
|
||||
default: {
|
||||
getDeepFilterNoiseSuppression: () => true,
|
||||
getDeepFilterNoiseSuppressionLevel: () => 80,
|
||||
},
|
||||
}));
|
||||
|
||||
const deepFilterHarness = vi.hoisted(() => ({
|
||||
instances: [] as Array<{
|
||||
audioContext: AudioContext | null;
|
||||
processedTrack: MediaStreamTrack | null;
|
||||
sampleRateAtInit: number | null;
|
||||
}>,
|
||||
}));
|
||||
|
||||
vi.mock('deepfilternet3-noise-filter', () => ({
|
||||
DeepFilterNoiseFilterProcessor: class {
|
||||
audioContext: AudioContext | null = null;
|
||||
processedTrack: MediaStreamTrack | null = null;
|
||||
sampleRateAtInit: number | null = null;
|
||||
|
||||
constructor() {
|
||||
deepFilterHarness.instances.push(this);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.sampleRateAtInit = this.audioContext?.sampleRate ?? null;
|
||||
this.processedTrack = {} as MediaStreamTrack;
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {}
|
||||
},
|
||||
}));
|
||||
|
||||
const MODEL_SAMPLE_RATE = 48000;
|
||||
|
||||
interface ContextHarness {
|
||||
requestedRates: Array<number | undefined>;
|
||||
closedRates: Array<number>;
|
||||
}
|
||||
|
||||
function installAudioGraph(config: {hardwareSampleRate: number; canBridgeSampleRates: boolean}): ContextHarness {
|
||||
const harness: ContextHarness = {requestedRates: [], closedRates: []};
|
||||
|
||||
class FakeAudioContext {
|
||||
state: AudioContextState = 'running';
|
||||
sampleRate: number;
|
||||
|
||||
constructor(options?: AudioContextOptions) {
|
||||
harness.requestedRates.push(options?.sampleRate);
|
||||
this.sampleRate = options?.sampleRate ?? config.hardwareSampleRate;
|
||||
}
|
||||
|
||||
createMediaStreamSource(): {disconnect: () => void} {
|
||||
if (!config.canBridgeSampleRates && this.sampleRate !== config.hardwareSampleRate) {
|
||||
throw new DOMException(
|
||||
'Connecting AudioNodes from AudioContexts with different sample-rate is currently not supported.',
|
||||
'NotSupportedError',
|
||||
);
|
||||
}
|
||||
return {disconnect: (): void => undefined};
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
harness.closedRates.push(this.sampleRate);
|
||||
this.state = 'closed';
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('window', {AudioContext: FakeAudioContext});
|
||||
vi.stubGlobal('MediaStream', class {});
|
||||
return harness;
|
||||
}
|
||||
|
||||
async function loadModule() {
|
||||
vi.resetModules();
|
||||
return await import('@app/features/voice/utils/DeepFilterNoiseProcessor');
|
||||
}
|
||||
|
||||
function createSourceContext(sampleRate: number): AudioContext {
|
||||
return {sampleRate} as unknown as AudioContext;
|
||||
}
|
||||
|
||||
const feedTrack = {} as MediaStreamTrack;
|
||||
|
||||
describe('resolveDeepFilterAudioContext', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('runs the worklet at the model rate when the capture graph already runs there', async () => {
|
||||
const harness = installAudioGraph({hardwareSampleRate: MODEL_SAMPLE_RATE, canBridgeSampleRates: false});
|
||||
const {resolveDeepFilterAudioContext} = await loadModule();
|
||||
|
||||
const context = resolveDeepFilterAudioContext(createSourceContext(MODEL_SAMPLE_RATE), feedTrack);
|
||||
|
||||
expect(context?.sampleRate).toBe(MODEL_SAMPLE_RATE);
|
||||
expect(harness.requestedRates).toEqual([MODEL_SAMPLE_RATE]);
|
||||
expect(harness.closedRates).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the model rate on browsers that resample across graphs', async () => {
|
||||
const harness = installAudioGraph({hardwareSampleRate: 44100, canBridgeSampleRates: true});
|
||||
const {resolveDeepFilterAudioContext} = await loadModule();
|
||||
|
||||
const context = resolveDeepFilterAudioContext(createSourceContext(44100), feedTrack);
|
||||
|
||||
expect(context?.sampleRate).toBe(MODEL_SAMPLE_RATE);
|
||||
expect(harness.requestedRates).toEqual([MODEL_SAMPLE_RATE]);
|
||||
expect(harness.closedRates).toEqual([]);
|
||||
});
|
||||
|
||||
it('falls back to the capture rate when the browser refuses to bridge sample rates', async () => {
|
||||
const harness = installAudioGraph({hardwareSampleRate: 44100, canBridgeSampleRates: false});
|
||||
const {resolveDeepFilterAudioContext} = await loadModule();
|
||||
|
||||
const context = resolveDeepFilterAudioContext(createSourceContext(44100), feedTrack);
|
||||
|
||||
expect(context?.sampleRate).toBe(44100);
|
||||
expect(harness.requestedRates).toEqual([MODEL_SAMPLE_RATE, 44100]);
|
||||
expect(harness.closedRates).toEqual([MODEL_SAMPLE_RATE]);
|
||||
});
|
||||
|
||||
it('falls back to the capture rate for a bluetooth headset running well below the model rate', async () => {
|
||||
installAudioGraph({hardwareSampleRate: 16000, canBridgeSampleRates: false});
|
||||
const {resolveDeepFilterAudioContext} = await loadModule();
|
||||
|
||||
const context = resolveDeepFilterAudioContext(createSourceContext(16000), feedTrack);
|
||||
|
||||
expect(context?.sampleRate).toBe(16000);
|
||||
});
|
||||
});
|
||||
|
||||
function createCaptureContext(sampleRate: number): AudioContext {
|
||||
const track = {stop: (): void => undefined} as unknown as MediaStreamTrack;
|
||||
const node = {
|
||||
connect: (next: unknown): unknown => next,
|
||||
disconnect: (): void => undefined,
|
||||
stream: {getAudioTracks: (): Array<MediaStreamTrack> => [track]},
|
||||
type: '',
|
||||
frequency: {value: 0},
|
||||
Q: {value: 0},
|
||||
threshold: {value: 0},
|
||||
knee: {value: 0},
|
||||
ratio: {value: 0},
|
||||
attack: {value: 0},
|
||||
release: {value: 0},
|
||||
};
|
||||
return {
|
||||
sampleRate,
|
||||
createMediaStreamDestination: () => node,
|
||||
createMediaStreamSource: () => node,
|
||||
createBiquadFilter: () => node,
|
||||
createDynamicsCompressor: () => node,
|
||||
} as unknown as AudioContext;
|
||||
}
|
||||
|
||||
describe('buildDeepFilterAudioChain', () => {
|
||||
beforeEach(() => {
|
||||
deepFilterHarness.instances.length = 0;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('gives the processor its audio context before the processor builds its own graph', async () => {
|
||||
installAudioGraph({hardwareSampleRate: MODEL_SAMPLE_RATE, canBridgeSampleRates: false});
|
||||
const {buildDeepFilterAudioChain} = await loadModule();
|
||||
|
||||
await buildDeepFilterAudioChain({audioContext: createCaptureContext(MODEL_SAMPLE_RATE)});
|
||||
|
||||
expect(deepFilterHarness.instances[0]?.sampleRateAtInit).toBe(MODEL_SAMPLE_RATE);
|
||||
});
|
||||
|
||||
it('gives the processor a capture-rate context when the browser refuses to bridge sample rates', async () => {
|
||||
installAudioGraph({hardwareSampleRate: 44100, canBridgeSampleRates: false});
|
||||
const {buildDeepFilterAudioChain} = await loadModule();
|
||||
|
||||
await buildDeepFilterAudioChain({audioContext: createCaptureContext(44100)});
|
||||
|
||||
expect(deepFilterHarness.instances[0]?.sampleRateAtInit).toBe(44100);
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const master = {connect: vi.fn(), disconnect: vi.fn()};
|
||||
const createdGains: Array<{connect: ReturnType<typeof vi.fn>; disconnect: ReturnType<typeof vi.fn>}> = [];
|
||||
let captureActive = true;
|
||||
|
||||
vi.mock('@app/features/notification/utils/SoundUtils', () => ({
|
||||
getSoundCaptureAudioContext: () => ({
|
||||
createMediaElementSource: vi.fn(() => ({connect: vi.fn(), disconnect: vi.fn()})),
|
||||
createGain: vi.fn(() => {
|
||||
const gain = {connect: vi.fn(), disconnect: vi.fn(), gain: {value: 1}};
|
||||
createdGains.push(gain);
|
||||
return gain;
|
||||
}),
|
||||
}),
|
||||
getSoundCaptureMasterGainNode: () => master,
|
||||
isSoundCaptureActive: () => captureActive,
|
||||
onSoundCaptureActivated: () => () => undefined,
|
||||
}));
|
||||
|
||||
const {routeMediaElementForSoundCapture} = await import('@app/features/voice/utils/InAppMediaSoundCapture');
|
||||
|
||||
function createElement(): HTMLMediaElement {
|
||||
const listeners = new Map<string, () => void>();
|
||||
return {
|
||||
volume: 1,
|
||||
muted: false,
|
||||
addEventListener: (type: string, listener: () => void) => listeners.set(type, listener),
|
||||
removeEventListener: (type: string) => listeners.delete(type),
|
||||
listenerCount: () => listeners.size,
|
||||
} as unknown as HTMLMediaElement & {listenerCount: () => number};
|
||||
}
|
||||
|
||||
describe('in-app media sound capture', () => {
|
||||
beforeEach(() => {
|
||||
createdGains.length = 0;
|
||||
captureActive = true;
|
||||
});
|
||||
|
||||
it('disconnects the element from the master graph when the caller disposes', () => {
|
||||
const element = createElement();
|
||||
const dispose = routeMediaElementForSoundCapture(element);
|
||||
const gain = createdGains.at(-1);
|
||||
expect(gain?.connect).toHaveBeenCalledWith(master);
|
||||
dispose();
|
||||
expect(gain?.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes the volume listener when the caller disposes', () => {
|
||||
const element = createElement() as HTMLMediaElement & {listenerCount: () => number};
|
||||
const dispose = routeMediaElementForSoundCapture(element);
|
||||
expect(element.listenerCount()).toBe(1);
|
||||
dispose();
|
||||
expect(element.listenerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('reuses the existing source node when the same element is routed again', () => {
|
||||
const element = createElement();
|
||||
routeMediaElementForSoundCapture(element)();
|
||||
const gainsAfterFirst = createdGains.length;
|
||||
const dispose = routeMediaElementForSoundCapture(element);
|
||||
expect(createdGains.length).toBe(gainsAfterFirst);
|
||||
expect(createdGains.at(-1)?.connect).toHaveBeenCalledTimes(2);
|
||||
dispose();
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL,
|
||||
resolveVoiceProcessing,
|
||||
type VoiceProcessingSettingsLike,
|
||||
} from '@app/features/voice/utils/VoiceProcessingProfile';
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/voice/engine/VoiceDevicePermissionState', () => ({
|
||||
default: {getState: () => ({inputDevices: []})},
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/voice/utils/VoiceDeviceManager', () => ({
|
||||
resolveEffectiveDeviceId: () => null,
|
||||
}));
|
||||
|
||||
function createSettings(overrides: Partial<VoiceProcessingSettingsLike> = {}): VoiceProcessingSettingsLike {
|
||||
return {
|
||||
voiceProcessingMode: 'voice',
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
deepFilterNoiseSuppression: true,
|
||||
deepFilterNoiseSuppressionLevel: 80,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('resolveVoiceProcessing', () => {
|
||||
it('keeps automatic gain control on for focused voice at factory defaults', () => {
|
||||
const resolved = resolveVoiceProcessing(createSettings());
|
||||
expect(resolved.mode).toBe('voice');
|
||||
expect(resolved.autoGainControl).toBe(true);
|
||||
expect(resolved.deepFilter).toBe(true);
|
||||
expect(resolved.deepFilterNoiseReductionLevel).toBe(FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL);
|
||||
});
|
||||
|
||||
it('honours an explicit opt out of automatic gain control in focused voice', () => {
|
||||
const resolved = resolveVoiceProcessing(createSettings({autoGainControl: false}));
|
||||
expect(resolved.autoGainControl).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps automatic gain control on in custom mode while deep filter is enabled', () => {
|
||||
const resolved = resolveVoiceProcessing(
|
||||
createSettings({voiceProcessingMode: 'custom', deepFilterNoiseSuppression: true}),
|
||||
);
|
||||
expect(resolved.autoGainControl).toBe(true);
|
||||
expect(resolved.deepFilter).toBe(true);
|
||||
});
|
||||
|
||||
it('still suppresses browser noise suppression when deep filter is enabled in custom mode', () => {
|
||||
const resolved = resolveVoiceProcessing(
|
||||
createSettings({voiceProcessingMode: 'custom', noiseSuppression: true, deepFilterNoiseSuppression: true}),
|
||||
);
|
||||
expect(resolved.browserNoiseSuppression).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves studio mode as the untouched bypass', () => {
|
||||
const resolved = resolveVoiceProcessing(createSettings({voiceProcessingMode: 'studio'}));
|
||||
expect(resolved.autoGainControl).toBe(false);
|
||||
expect(resolved.echoCancellation).toBe(false);
|
||||
expect(resolved.browserNoiseSuppression).toBe(false);
|
||||
expect(resolved.deepFilter).toBe(false);
|
||||
expect(resolved.contentHint).toBe('music');
|
||||
});
|
||||
});
|
||||
@@ -419,7 +419,7 @@ function handleIncomingTtsMessage(message: Message): void {
|
||||
}
|
||||
if (
|
||||
UserGuildSettings.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) ||
|
||||
UserGuildSettings.isCategoryMuted(channel.guildId ?? null, channel.id)
|
||||
UserGuildSettings.isParentCategoryMuted(channel.guildId ?? null, channel.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Notification from '@app/features/ui/state/Notification';
|
||||
import Window from '@app/features/window/state/Window';
|
||||
|
||||
function applyFocusSideEffects(focused: boolean): void {
|
||||
GuildReadState.handleWindowFocus();
|
||||
GuildReadState.handleWindowFocused();
|
||||
Notification.handleWindowFocus({focused});
|
||||
if (focused) {
|
||||
Idle.recordActivity();
|
||||
|
||||
Reference in new issue
Block a user