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(() => {
|
export const AppBadge: React.FC = observer(() => {
|
||||||
const relationships = Relationships.getRelationships();
|
const relationships = Relationships.getRelationships();
|
||||||
const unreadMessageBadgeEnabled = Notification.unreadMessageBadgeEnabled;
|
const unreadMessageBadgeEnabled = Notification.unreadMessageBadgeEnabled;
|
||||||
const mentionCount = GuildReadState.getTotalMentionCount();
|
const mentionCount = GuildReadState.mentionCountAcrossGuilds();
|
||||||
const hasUnread = GuildReadState.hasAnyUnread;
|
const hasUnread = GuildReadState.anyGuildUnread;
|
||||||
const pendingCount = RuntimeConfig.directMessagesDisabled
|
const pendingCount = RuntimeConfig.directMessagesDisabled
|
||||||
? 0
|
? 0
|
||||||
: relationships.filter((relationship) => relationship.type === RelationshipTypes.INCOMING_REQUEST).length;
|
: relationships.filter((relationship) => relationship.type === RelationshipTypes.INCOMING_REQUEST).length;
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ export const FavoritesGuildHeaderBottomSheet: React.FC<FavoritesGuildHeaderBotto
|
|||||||
({isOpen, onClose}) => {
|
({isOpen, onClose}) => {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const hideMutedChannels = Favorites.hideMutedChannels;
|
const hideMutedChannels = Favorites.hideMutedChannels;
|
||||||
const settings = UserGuildSettings.getSettings(FAVORITES_GUILD_ID);
|
const settings = UserGuildSettings.getSettingsForScope(FAVORITES_GUILD_ID);
|
||||||
const isMuted = settings?.muted ?? false;
|
const isMuted = settings?.muted ?? false;
|
||||||
const handleAddChannel = () => {
|
const handleAddChannel = () => {
|
||||||
ModalCommands.pushAfterBottomSheetClose(
|
ModalCommands.pushAfterBottomSheetClose(
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ const UnreadChannelCard = observer(function UnreadChannelCard({
|
|||||||
const messageGroupSpacing = Accessibility.getMessageGroupSpacingValue(messageDisplayCompact);
|
const messageGroupSpacing = Accessibility.getMessageGroupSpacingValue(messageDisplayCompact);
|
||||||
const mentionCount = ReadStates.getMentionCount(channel.id);
|
const mentionCount = ReadStates.getMentionCount(channel.id);
|
||||||
const isCollapsed = UnreadChannels.isCollapsed(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 previewMessages = previewData?.messages ?? [];
|
||||||
const oldestUnreadMessageId = previewData?.oldestUnreadMessageId ?? null;
|
const oldestUnreadMessageId = previewData?.oldestUnreadMessageId ?? null;
|
||||||
const totalUnreadCount = ReadStates.getUnreadCount(channel.id);
|
const totalUnreadCount = ReadStates.getUnreadCount(channel.id);
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export const ChannelItem = observer(
|
|||||||
const canInvite = InviteUtils.canInviteToChannel(channel.id, channel.guildId);
|
const canInvite = InviteUtils.canInviteToChannel(channel.id, channel.guildId);
|
||||||
const mobileLayout = MobileLayout;
|
const mobileLayout = MobileLayout;
|
||||||
const isMuted = UserGuildSettings.isGuildOrChannelMuted(guild.id, channel.id);
|
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 =
|
const currentUserCount =
|
||||||
channelIsVoice && channel.userLimit != null && channel.userLimit > 0
|
channelIsVoice && channel.userLimit != null && channel.userLimit > 0
|
||||||
? Object.keys(MediaEngine.getAllVoiceStatesInChannel(guild.id, channel.id)).length
|
? 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 {i18n} = useLingui();
|
||||||
const channels = Channels.getGuildChannels(guild.id);
|
const channels = Channels.getGuildChannels(guild.id);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const userGuildSettings = UserGuildSettings.getSettings(guild.id);
|
const userGuildSettings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||||
const isDraggingAnything = useDragLayer((monitor) => {
|
const isDraggingAnything = useDragLayer((monitor) => {
|
||||||
if (!monitor.isDragging()) return false;
|
if (!monitor.isDragging()) return false;
|
||||||
const itemType = monitor.getItemType();
|
const itemType = monitor.getItemType();
|
||||||
@@ -337,7 +337,7 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
|||||||
const unreadCount = ReadStates.getUnreadCount(channelId);
|
const unreadCount = ReadStates.getUnreadCount(channelId);
|
||||||
const mentionCount = ReadStates.getMentionCount(channelId);
|
const mentionCount = ReadStates.getMentionCount(channelId);
|
||||||
const isMuted =
|
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 channel = Channels.getChannel(channelId);
|
||||||
const unreadBadgesLevel = channel
|
const unreadBadgesLevel = channel
|
||||||
? UserGuildSettings.resolvedUnreadBadgesLevel({
|
? UserGuildSettings.resolvedUnreadBadgesLevel({
|
||||||
@@ -360,13 +360,13 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
|||||||
for (const group of channelGroups) {
|
for (const group of channelGroups) {
|
||||||
const isCollapsed = group.category ? (collapsedCategories?.has(group.category.id) ?? false) : false;
|
const isCollapsed = group.category ? (collapsedCategories?.has(group.category.id) ?? false) : false;
|
||||||
const isNullSpace = !group.category;
|
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 filteredTextChannels: Array<Channel>;
|
||||||
let filteredVoiceChannels: Array<Channel>;
|
let filteredVoiceChannels: Array<Channel>;
|
||||||
if (hideMutedChannels) {
|
if (hideMutedChannels) {
|
||||||
filteredTextChannels = [];
|
filteredTextChannels = [];
|
||||||
for (const ch of group.textChannels) {
|
for (const ch of group.textChannels) {
|
||||||
const isMuted = UserGuildSettings.isChannelMuted(guild.id, ch.id);
|
const isMuted = UserGuildSettings.isChannelDirectlyMuted(guild.id, ch.id);
|
||||||
if (
|
if (
|
||||||
shouldShowChannelWhenHidingMutedChannels({
|
shouldShowChannelWhenHidingMutedChannels({
|
||||||
isMuted,
|
isMuted,
|
||||||
@@ -380,7 +380,7 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
|||||||
}
|
}
|
||||||
filteredVoiceChannels = [];
|
filteredVoiceChannels = [];
|
||||||
for (const ch of group.voiceChannels) {
|
for (const ch of group.voiceChannels) {
|
||||||
const isMuted = UserGuildSettings.isChannelMuted(guild.id, ch.id);
|
const isMuted = UserGuildSettings.isChannelDirectlyMuted(guild.id, ch.id);
|
||||||
if (
|
if (
|
||||||
shouldShowChannelWhenHidingMutedChannels({
|
shouldShowChannelWhenHidingMutedChannels({
|
||||||
isMuted,
|
isMuted,
|
||||||
|
|||||||
@@ -206,11 +206,11 @@ const FavoriteChannelResolvedItem = observer(
|
|||||||
const channelDisplayName = channel.isPrivate() ? ChannelUtils.getDMDisplayName(channel) : channel.name;
|
const channelDisplayName = channel.isPrivate() ? ChannelUtils.getDMDisplayName(channel) : channel.name;
|
||||||
const displayName = favoriteChannel.nickname || channelDisplayName || i18n._(UNKNOWN_CHANNEL_DESCRIPTOR);
|
const displayName = favoriteChannel.nickname || channelDisplayName || i18n._(UNKNOWN_CHANNEL_DESCRIPTOR);
|
||||||
const isChannelDirectlyMuted = channel.guildId
|
const isChannelDirectlyMuted = channel.guildId
|
||||||
? UserGuildSettings.isChannelMuted(channel.guildId, channel.id)
|
? UserGuildSettings.isChannelDirectlyMuted(channel.guildId, channel.id)
|
||||||
: false;
|
: false;
|
||||||
const isMuted =
|
const isMuted =
|
||||||
isChannelDirectlyMuted ||
|
isChannelDirectlyMuted ||
|
||||||
(channel.guildId ? UserGuildSettings.isCategoryMuted(channel.guildId, channel.id) : false);
|
(channel.guildId ? UserGuildSettings.isParentCategoryMuted(channel.guildId, channel.id) : false);
|
||||||
const unreadBadgesLevel = channel.guildId
|
const unreadBadgesLevel = channel.guildId
|
||||||
? UserGuildSettings.resolvedUnreadBadgesLevel({
|
? UserGuildSettings.resolvedUnreadBadgesLevel({
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
|
|||||||
@@ -665,7 +665,7 @@ function getDMScrollIndicatorSeverity(channelId: string): ScrollIndicatorSeverit
|
|||||||
const unreadState = getChannelUnreadState({
|
const unreadState = getChannelUnreadState({
|
||||||
unreadCount: ReadStates.getPrivateChannelUnreadCount(channelId),
|
unreadCount: ReadStates.getPrivateChannelUnreadCount(channelId),
|
||||||
mentionCount,
|
mentionCount,
|
||||||
isMuted: UserGuildSettings.isChannelMuted(null, channelId),
|
isMuted: UserGuildSettings.isChannelDirectlyMuted(null, channelId),
|
||||||
showFadedUnreadOnMutedChannels: Accessibility.showFadedUnreadOnMutedChannels,
|
showFadedUnreadOnMutedChannels: Accessibility.showFadedUnreadOnMutedChannels,
|
||||||
});
|
});
|
||||||
if (mentionCount > 0) return 'mention';
|
if (mentionCount > 0) return 'mention';
|
||||||
@@ -674,8 +674,8 @@ function getDMScrollIndicatorSeverity(channelId: string): ScrollIndicatorSeverit
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getGuildScrollIndicatorSeverity(guildId: string): ScrollIndicatorSeverity | null {
|
function getGuildScrollIndicatorSeverity(guildId: string): ScrollIndicatorSeverity | null {
|
||||||
if (GuildReadState.getMentionCount(guildId) > 0) return 'mention';
|
if (GuildReadState.mentionCountForGuild(guildId) > 0) return 'mention';
|
||||||
if (GuildReadState.hasUnread(guildId)) return 'unread';
|
if (GuildReadState.guildIsUnread(guildId)) return 'unread';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1327,10 +1327,10 @@ const GUILD_RAIL_INDICATORS_BY_TOKEN: Record<string, SkeletonGuildRailItemIndica
|
|||||||
function resolveGuildRailIndicator(guildIds: ReadonlyArray<string>): SkeletonGuildRailItemIndicator {
|
function resolveGuildRailIndicator(guildIds: ReadonlyArray<string>): SkeletonGuildRailItemIndicator {
|
||||||
let hasUnread = false;
|
let hasUnread = false;
|
||||||
for (const guildId of guildIds) {
|
for (const guildId of guildIds) {
|
||||||
if (GuildReadState.getMentionCount(guildId) > 0) {
|
if (GuildReadState.mentionCountForGuild(guildId) > 0) {
|
||||||
return SkeletonGuildRailItemIndicator.MENTION;
|
return SkeletonGuildRailItemIndicator.MENTION;
|
||||||
}
|
}
|
||||||
if (GuildReadState.hasUnread(guildId)) {
|
if (GuildReadState.guildIsUnread(guildId)) {
|
||||||
hasUnread = true;
|
hasUnread = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,8 +316,8 @@ export const GuildFolderItem = observer((props: GuildFolderItemProps) => {
|
|||||||
}) as CSSProperties,
|
}) as CSSProperties,
|
||||||
[folderColor],
|
[folderColor],
|
||||||
);
|
);
|
||||||
const hasUnreadMessages = guilds.some((guild) => GuildReadState.hasUnread(guild.id));
|
const hasUnreadMessages = guilds.some((guild) => GuildReadState.guildIsUnread(guild.id));
|
||||||
const totalMentionCount = guilds.reduce((sum, guild) => sum + GuildReadState.getMentionCount(guild.id), 0);
|
const totalMentionCount = guilds.reduce((sum, guild) => sum + GuildReadState.mentionCountForGuild(guild.id), 0);
|
||||||
let folderScrollTargetRef: React.RefCallback<HTMLElement> | null = null;
|
let folderScrollTargetRef: React.RefCallback<HTMLElement> | null = null;
|
||||||
if (registerScrollTarget != null) {
|
if (registerScrollTarget != null) {
|
||||||
folderScrollTargetRef = registerScrollTarget(folderId);
|
folderScrollTargetRef = registerScrollTarget(folderId);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function resolveDMListItemUnreadState(channelId: string): ChannelUnreadSt
|
|||||||
return getChannelUnreadState({
|
return getChannelUnreadState({
|
||||||
unreadCount: ReadStates.getUnreadCount(channelId),
|
unreadCount: ReadStates.getUnreadCount(channelId),
|
||||||
mentionCount: ReadStates.getMentionCount(channelId),
|
mentionCount: ReadStates.getMentionCount(channelId),
|
||||||
isMuted: UserGuildSettings.isChannelMuted(null, channelId),
|
isMuted: UserGuildSettings.isChannelDirectlyMuted(null, channelId),
|
||||||
showFadedUnreadOnMutedChannels: Accessibility.showFadedUnreadOnMutedChannels,
|
showFadedUnreadOnMutedChannels: Accessibility.showFadedUnreadOnMutedChannels,
|
||||||
unreadBadgesLevel: null,
|
unreadBadgesLevel: null,
|
||||||
});
|
});
|
||||||
@@ -124,7 +124,7 @@ const ResolvedDMListItem = observer(function ResolvedDMListItem({
|
|||||||
const contextMenuOpen = useContextMenuHoverState(buttonRef, !isMobileExperience);
|
const contextMenuOpen = useContextMenuHoverState(buttonRef, !isMobileExperience);
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
const {keyboardModeEnabled} = KeyboardMode;
|
const {keyboardModeEnabled} = KeyboardMode;
|
||||||
const isMuted = UserGuildSettings.isChannelMuted(null, channel.id);
|
const isMuted = UserGuildSettings.isChannelDirectlyMuted(null, channel.id);
|
||||||
const mentionCount = ReadStates.getMentionCount(channel.id);
|
const mentionCount = ReadStates.getMentionCount(channel.id);
|
||||||
const unreadState = resolveDMListItemUnreadState(channel.id);
|
const unreadState = resolveDMListItemUnreadState(channel.id);
|
||||||
const directMessageName = recipient ? NicknameUtils.getNickname(recipient, null, channel.id) : null;
|
const directMessageName = recipient ? NicknameUtils.getNickname(recipient, null, channel.id) : null;
|
||||||
|
|||||||
@@ -107,9 +107,9 @@ export function useGuildListItemState({
|
|||||||
isSelected,
|
isSelected,
|
||||||
selectedGuildIndex,
|
selectedGuildIndex,
|
||||||
}: UseGuildListItemStateOptions): GuildListItemState {
|
}: UseGuildListItemStateOptions): GuildListItemState {
|
||||||
const hasUnreadMessages = GuildReadState.hasUnread(guild.id);
|
const hasUnreadMessages = GuildReadState.guildIsUnread(guild.id);
|
||||||
const mentionCount = GuildReadState.getMentionCount(guild.id);
|
const mentionCount = GuildReadState.mentionCountForGuild(guild.id);
|
||||||
const settings = UserGuildSettings.getSettings(guild.id);
|
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||||
const isMuted = settings.muted;
|
const isMuted = settings.muted;
|
||||||
const muteConfig = settings.mute_config;
|
const muteConfig = settings.mute_config;
|
||||||
let muteEndTime: string | null = null;
|
let muteEndTime: string | null = null;
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ const ResolvedDMListItem = observer(function ResolvedDMListItem({
|
|||||||
: TypingIndicator.isTyping(channel.id, recipientId);
|
: TypingIndicator.isTyping(channel.id, recipientId);
|
||||||
const hasUnreadMessages = () => ReadStates.hasUnread(channel.id);
|
const hasUnreadMessages = () => ReadStates.hasUnread(channel.id);
|
||||||
const isMobile = MobileLayout.isMobileLayout();
|
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 mentionCount = ReadStates.getMentionCount(channel.id);
|
||||||
const unreadState = getChannelUnreadState({
|
const unreadState = getChannelUnreadState({
|
||||||
unreadCount: ReadStates.getUnreadCount(channel.id),
|
unreadCount: ReadStates.getUnreadCount(channel.id),
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export const GuildNotificationSettingsModal = observer(({guildId}: {guildId: str
|
|||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const muteChannelLabel = i18n._(MUTE_CHANNEL_DESCRIPTOR);
|
const muteChannelLabel = i18n._(MUTE_CHANNEL_DESCRIPTOR);
|
||||||
const guild = Guilds.getGuild(guildId);
|
const guild = Guilds.getGuild(guildId);
|
||||||
const settings = UserGuildSettings.getSettings(guildId);
|
const settings = UserGuildSettings.getSettingsForScope(guildId);
|
||||||
if (!guild || !settings) return null;
|
if (!guild || !settings) return null;
|
||||||
const channels = Channels.getGuildChannels(guildId);
|
const channels = Channels.getGuildChannels(guildId);
|
||||||
const categories = channels.filter((c) => c.type === ChannelTypes.GUILD_CATEGORY);
|
const categories = channels.filter((c) => c.type === ChannelTypes.GUILD_CATEGORY);
|
||||||
|
|||||||
@@ -171,11 +171,11 @@ export const GuildHeaderPopout = observer(({guild}: {guild: Guild}) => {
|
|||||||
canCreateExpressions ||
|
canCreateExpressions ||
|
||||||
canBanMembers;
|
canBanMembers;
|
||||||
const canEditCommunityProfile = Users.getCurrentUser()?.isClaimed() ?? true;
|
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 hideMutedChannels = settings?.hide_muted_channels ?? false;
|
||||||
const handleToggleHideMutedChannels = useCallback(
|
const handleToggleHideMutedChannels = useCallback(
|
||||||
(checked: boolean) => {
|
(checked: boolean) => {
|
||||||
const currentSettings = UserGuildSettings.getSettings(guild.id);
|
const currentSettings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||||
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
||||||
if (checked === currentValue) return;
|
if (checked === currentValue) return;
|
||||||
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ const PRIVATE_CHANNEL_SENTINEL = ME;
|
|||||||
|
|
||||||
class GuildReadStateEntry {
|
class GuildReadStateEntry {
|
||||||
unread = observable.box(false);
|
unread = observable.box(false);
|
||||||
unreadChannelId = observable.box<ChannelId | null>(null);
|
firstUnreadChannelId = observable.box<ChannelId | null>(null);
|
||||||
mentionCount = observable.box(0);
|
mentionCount = observable.box(0);
|
||||||
mentionChannels = observable.set(new Set<ChannelId>());
|
mentionChannels = observable.set(new Set<ChannelId>());
|
||||||
sentinel = observable.box(0);
|
changeTicket = observable.box(0);
|
||||||
|
|
||||||
incrementSentinel(): void {
|
bumpChangeTicket(): void {
|
||||||
this.sentinel.set(this.sentinel.get() + 1);
|
this.changeTicket.set(this.changeTicket.get() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.unread.set(false);
|
this.unread.set(false);
|
||||||
this.unreadChannelId.set(null);
|
this.firstUnreadChannelId.set(null);
|
||||||
this.mentionCount.set(0);
|
this.mentionCount.set(0);
|
||||||
this.mentionChannels.clear();
|
this.mentionChannels.clear();
|
||||||
}
|
}
|
||||||
@@ -43,12 +43,12 @@ type ContributeChannel = {
|
|||||||
guildId?: string | null;
|
guildId?: string | null;
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
isPrivate(): boolean;
|
isPrivate(): boolean;
|
||||||
isGuildVocal?(): boolean;
|
isGuildAudioChannel?(): boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isChannelMutedForUnread(channel: ContributeChannel): boolean {
|
function isChannelMutedForUnread(channel: ContributeChannel): boolean {
|
||||||
if (channel.isPrivate()) return false;
|
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 {
|
function resolveUnreadBadgesLevel(channel: ContributeChannel): number | null {
|
||||||
@@ -77,7 +77,7 @@ function getChannelContribution(channel: ContributeChannel, channelId: string):
|
|||||||
|
|
||||||
class GuildReadState {
|
class GuildReadState {
|
||||||
private readonly guildStates = observable.map(new Map<GuildId, GuildReadStateEntry>());
|
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;
|
updateCounter = 0;
|
||||||
private readStateReactionInstalled = false;
|
private readStateReactionInstalled = false;
|
||||||
|
|
||||||
@@ -156,9 +156,9 @@ class GuildReadState {
|
|||||||
this.updateCounter++;
|
this.updateCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private incrementSentinel(guildId: string | null): void {
|
private bumpChangeTicket(guildId: string | null): void {
|
||||||
const state = this.getOrCreate(guildId);
|
const state = this.getOrCreate(guildId);
|
||||||
state.incrementSentinel();
|
state.bumpChangeTicket();
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,18 +166,18 @@ class GuildReadState {
|
|||||||
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
||||||
const state = this.getOrCreate(id);
|
const state = this.getOrCreate(id);
|
||||||
const previousUnread = state.unread.get();
|
const previousUnread = state.unread.get();
|
||||||
const previousUnreadChannelId = state.unreadChannelId.get();
|
const previousFirstUnread = state.firstUnreadChannelId.get();
|
||||||
const previousMentionCount = state.mentionCount.get();
|
const previousMentionCount = state.mentionCount.get();
|
||||||
const mentionChannelUpdates = new Map<ChannelId, boolean>();
|
const mentionChannelUpdates = new Map<ChannelId, boolean>();
|
||||||
let foundUnread = false;
|
let foundUnread = false;
|
||||||
let shouldClearUnreadChannelId = false;
|
let shouldClearFirstUnread = false;
|
||||||
let nextUnreadChannelId = previousUnreadChannelId;
|
let nextFirstUnread = previousFirstUnread;
|
||||||
for (const channelId of channelIds) {
|
for (const channelId of channelIds) {
|
||||||
const channel = Channels.getChannel(channelId);
|
const channel = Channels.getChannel(channelId);
|
||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
mentionChannelUpdates.set(channelId, false);
|
mentionChannelUpdates.set(channelId, false);
|
||||||
if (nextUnreadChannelId === channelId) {
|
if (nextFirstUnread === channelId) {
|
||||||
shouldClearUnreadChannelId = true;
|
shouldClearFirstUnread = true;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -194,14 +194,14 @@ class GuildReadState {
|
|||||||
mentionChannelUpdates.set(channelId, contribution.mentionAllowed);
|
mentionChannelUpdates.set(channelId, contribution.mentionAllowed);
|
||||||
if (guildId != null && !foundUnread && contribution.unreadAllowed) {
|
if (guildId != null && !foundUnread && contribution.unreadAllowed) {
|
||||||
foundUnread = true;
|
foundUnread = true;
|
||||||
nextUnreadChannelId = channelId;
|
nextFirstUnread = channelId;
|
||||||
} else if (!contribution.unreadAllowed && nextUnreadChannelId === channelId) {
|
} else if (!contribution.unreadAllowed && nextFirstUnread === channelId) {
|
||||||
shouldClearUnreadChannelId = true;
|
shouldClearFirstUnread = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nextUnread = foundUnread;
|
const nextUnread = foundUnread;
|
||||||
if (!nextUnread && shouldClearUnreadChannelId) {
|
if (!nextUnread && shouldClearFirstUnread) {
|
||||||
nextUnreadChannelId = null;
|
nextFirstUnread = null;
|
||||||
}
|
}
|
||||||
if (previousUnread && !nextUnread) {
|
if (previousUnread && !nextUnread) {
|
||||||
return this.recomputeAll(guildId);
|
return this.recomputeAll(guildId);
|
||||||
@@ -219,9 +219,7 @@ class GuildReadState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const changed =
|
const changed =
|
||||||
nextUnread !== previousUnread ||
|
nextUnread !== previousUnread || nextFirstUnread !== previousFirstUnread || mentionTotal !== previousMentionCount;
|
||||||
nextUnreadChannelId !== previousUnreadChannelId ||
|
|
||||||
mentionTotal !== previousMentionCount;
|
|
||||||
if (!changed && mentionChannelUpdates.size === 0) {
|
if (!changed && mentionChannelUpdates.size === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -237,16 +235,16 @@ class GuildReadState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.unread.set(nextUnread);
|
state.unread.set(nextUnread);
|
||||||
state.unreadChannelId.set(nextUnreadChannelId);
|
state.firstUnreadChannelId.set(nextFirstUnread);
|
||||||
state.mentionCount.set(mentionTotal);
|
state.mentionCount.set(mentionTotal);
|
||||||
if (id !== PRIVATE_CHANNEL_SENTINEL) {
|
if (id !== PRIVATE_CHANNEL_SENTINEL) {
|
||||||
if (nextUnread) {
|
if (nextUnread) {
|
||||||
this.unreadGuilds.add(id as GuildId);
|
this.unreadGuildIds.add(id as GuildId);
|
||||||
} else {
|
} else {
|
||||||
this.unreadGuilds.delete(id as GuildId);
|
this.unreadGuildIds.delete(id as GuildId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.incrementSentinel(guildId);
|
this.bumpChangeTicket(guildId);
|
||||||
});
|
});
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
@@ -264,11 +262,11 @@ class GuildReadState {
|
|||||||
}
|
}
|
||||||
if (!newState.unread.get() && contribution.unreadAllowed) {
|
if (!newState.unread.get() && contribution.unreadAllowed) {
|
||||||
newState.unread.set(true);
|
newState.unread.set(true);
|
||||||
newState.unreadChannelId.set(channel.id as ChannelId);
|
newState.firstUnreadChannelId.set(channel.id as ChannelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const isGuildMuted = UserGuildSettings.isMuted(guildId);
|
const isGuildMuted = UserGuildSettings.isWholeGuildMuted(guildId);
|
||||||
if (isGuildMuted && skipIfMuted) {
|
if (isGuildMuted && skipIfMuted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -281,7 +279,7 @@ class GuildReadState {
|
|||||||
}
|
}
|
||||||
if (!newState.unread.get() && contribution.unreadAllowed) {
|
if (!newState.unread.get() && contribution.unreadAllowed) {
|
||||||
newState.unread.set(true);
|
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 {
|
private commitState(guildId: string, newState: GuildReadStateEntry, prevState: GuildReadStateEntry): boolean {
|
||||||
if (
|
if (
|
||||||
newState.unread.get() === prevState.unread.get() &&
|
newState.unread.get() === prevState.unread.get() &&
|
||||||
newState.unreadChannelId.get() === prevState.unreadChannelId.get() &&
|
newState.firstUnreadChannelId.get() === prevState.firstUnreadChannelId.get() &&
|
||||||
newState.mentionCount.get() === prevState.mentionCount.get()
|
newState.mentionCount.get() === prevState.mentionCount.get()
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
@@ -301,12 +299,12 @@ class GuildReadState {
|
|||||||
this.guildStates.set(guildId as GuildId, newState);
|
this.guildStates.set(guildId as GuildId, newState);
|
||||||
if (guildId !== PRIVATE_CHANNEL_SENTINEL) {
|
if (guildId !== PRIVATE_CHANNEL_SENTINEL) {
|
||||||
if (newState.unread.get()) {
|
if (newState.unread.get()) {
|
||||||
this.unreadGuilds.add(guildId as GuildId);
|
this.unreadGuildIds.add(guildId as GuildId);
|
||||||
} else {
|
} 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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -335,21 +333,21 @@ class GuildReadState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasAnyUnread(): boolean {
|
get anyGuildUnread(): boolean {
|
||||||
return this.unreadGuilds.size > 0;
|
return this.unreadGuildIds.size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasUnread(guildId: string): boolean {
|
guildIsUnread(guildId: string): boolean {
|
||||||
return this.unreadGuilds.has(guildId as GuildId);
|
return this.unreadGuildIds.has(guildId as GuildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMentionCount(guildId: string | null): number {
|
mentionCountForGuild(guildId: string | null): number {
|
||||||
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
||||||
const state = this.guildStates.get(id as GuildId);
|
const state = this.guildStates.get(id as GuildId);
|
||||||
return state?.mentionCount.get() ?? 0;
|
return state?.mentionCount.get() ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalMentionCount(excludePrivate = false): number {
|
mentionCountAcrossGuilds(excludePrivate = false): number {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (const [guildId, state] of this.guildStates.entries()) {
|
for (const [guildId, state] of this.guildStates.entries()) {
|
||||||
if (excludePrivate && guildId === PRIVATE_CHANNEL_SENTINEL) continue;
|
if (excludePrivate && guildId === PRIVATE_CHANNEL_SENTINEL) continue;
|
||||||
@@ -358,22 +356,22 @@ class GuildReadState {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrivateChannelMentionCount(): number {
|
directMessageMentionCount(): number {
|
||||||
const state = this.guildStates.get(PRIVATE_CHANNEL_SENTINEL as GuildId);
|
const state = this.guildStates.get(PRIVATE_CHANNEL_SENTINEL as GuildId);
|
||||||
return state?.mentionCount.get() ?? 0;
|
return state?.mentionCount.get() ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMentionCountForPrivateChannel(channelId: string): number {
|
directMessageMentionCountFor(channelId: string): number {
|
||||||
return ReadStates.getMentionCount(channelId);
|
return ReadStates.getMentionCount(channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuildChangeSentinel(guildId: string | null): number {
|
guildChangeTicketFor(guildId: string | null): number {
|
||||||
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
const id = guildId ?? PRIVATE_CHANNEL_SENTINEL;
|
||||||
const state = this.guildStates.get(id as GuildId);
|
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);
|
const channels = Channels.getGuildChannels(guildId);
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
if (!GUILD_TEXT_BASED_CHANNEL_TYPES.has(channel.type)) {
|
if (!GUILD_TEXT_BASED_CHANNEL_TYPES.has(channel.type)) {
|
||||||
@@ -388,7 +386,7 @@ class GuildReadState {
|
|||||||
|
|
||||||
handleConnectionOpen(): void {
|
handleConnectionOpen(): void {
|
||||||
this.guildStates.clear();
|
this.guildStates.clear();
|
||||||
this.unreadGuilds.clear();
|
this.unreadGuildIds.clear();
|
||||||
this.updateCounter = 0;
|
this.updateCounter = 0;
|
||||||
this.recomputeAll(null);
|
this.recomputeAll(null);
|
||||||
for (const guildId of Guilds.getGuildIds()) {
|
for (const guildId of Guilds.getGuildIds()) {
|
||||||
@@ -411,7 +409,7 @@ class GuildReadState {
|
|||||||
};
|
};
|
||||||
}): void {
|
}): void {
|
||||||
this.guildStates.delete(action.guild.id as GuildId);
|
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();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +457,7 @@ class GuildReadState {
|
|||||||
this.handleConnectionOpen();
|
this.handleConnectionOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowFocus(): void {
|
handleWindowFocused(): void {
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -553,12 +553,12 @@ export class Message {
|
|||||||
export const messageMentionsCurrentUser = (message: WireMessage): boolean => {
|
export const messageMentionsCurrentUser = (message: WireMessage): boolean => {
|
||||||
const channel = Channels.getChannel(message.channel_id);
|
const channel = Channels.getChannel(message.channel_id);
|
||||||
if (!channel) return false;
|
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)) {
|
if (message.mentions?.some((user) => user.id === Authentication.currentUserId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!channel.guildId) return false;
|
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);
|
const guild = Guilds.getGuild(channel.guildId);
|
||||||
if (!guild) return false;
|
if (!guild) return false;
|
||||||
const guildMember = GuildMembers.getMember(guild.id, Authentication.currentUserId);
|
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!`);
|
logger.warn(`${message.channelId} does not exist!`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const suppressEveryone = UserGuildSettings.isSuppressEveryoneEnabled(channel.guildId ?? null);
|
const suppressEveryone = UserGuildSettings.isEveryoneMentionSuppressed(channel.guildId ?? null);
|
||||||
const mentionEveryone = message.mentionEveryone && !suppressEveryone;
|
const mentionEveryone = message.mentionEveryone && !suppressEveryone;
|
||||||
if (mentionEveryone) {
|
if (mentionEveryone) {
|
||||||
return true;
|
return true;
|
||||||
@@ -35,6 +35,6 @@ export function isMentioned(user: User, message: Message): boolean {
|
|||||||
if (!guildMember) {
|
if (!guildMember) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const suppressRoles = UserGuildSettings.isSuppressRolesEnabled(channel.guildId);
|
const suppressRoles = UserGuildSettings.isRoleMentionSuppressed(channel.guildId);
|
||||||
return !suppressRoles && message.mentionRoles.some((roleId) => guildMember.roles.has(roleId));
|
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 additionalMutePayload = isGuildMode ? undefined : params.additionalMutePayload;
|
||||||
const {onMuteSuccess, onUnmuteSuccess, onClose} = params;
|
const {onMuteSuccess, onUnmuteSuccess, onClose} = params;
|
||||||
const muteConfig = isGuildMode
|
const muteConfig = isGuildMode
|
||||||
? UserGuildSettings.getSettings(guildId)?.mute_config
|
? UserGuildSettings.getSettingsForScope(guildId)?.mute_config
|
||||||
: UserGuildSettings.getChannelOverride(guildId, channelId!)?.mute_config;
|
: UserGuildSettings.getChannelOverride(guildId, channelId!)?.mute_config;
|
||||||
const openMuteSheet = useCallback(() => {
|
const openMuteSheet = useCallback(() => {
|
||||||
setMuteSheetOpen(true);
|
setMuteSheetOpen(true);
|
||||||
|
|||||||
@@ -231,8 +231,8 @@ export class ReadStateEntry {
|
|||||||
const userId = currentUser.id;
|
const userId = currentUser.id;
|
||||||
const guildId = this.guildId;
|
const guildId = this.guildId;
|
||||||
const channelId = this.channelId;
|
const channelId = this.channelId;
|
||||||
const suppressEveryone = recomputeMentions ? UserGuildSettings.isSuppressEveryoneEnabled(guildId) : false;
|
const suppressEveryone = recomputeMentions ? UserGuildSettings.isEveryoneMentionSuppressed(guildId) : false;
|
||||||
const suppressRoles = recomputeMentions ? UserGuildSettings.isSuppressRolesEnabled(guildId) : false;
|
const suppressRoles = recomputeMentions ? UserGuildSettings.isRoleMentionSuppressed(guildId) : false;
|
||||||
const isMuted = recomputeMentions ? UserGuildSettings.isGuildOrChannelMuted(guildId, channelId) : false;
|
const isMuted = recomputeMentions ? UserGuildSettings.isGuildOrChannelMuted(guildId, channelId) : false;
|
||||||
const member = recomputeMentions && guildId ? GuildMembers.getMember(guildId, userId) : null;
|
const member = recomputeMentions && guildId ? GuildMembers.getMember(guildId, userId) : null;
|
||||||
const memberRoles = member?.roles ?? null;
|
const memberRoles = member?.roles ?? null;
|
||||||
@@ -285,8 +285,8 @@ export class ReadStateEntry {
|
|||||||
|
|
||||||
shouldMentionFor(message: MessageModel | WireMessage, userId: string, isPrivate: boolean): boolean {
|
shouldMentionFor(message: MessageModel | WireMessage, userId: string, isPrivate: boolean): boolean {
|
||||||
const authorBlocked = Relationships.isBlocked(message.author.id);
|
const authorBlocked = Relationships.isBlocked(message.author.id);
|
||||||
const suppressEveryone = UserGuildSettings.isSuppressEveryoneEnabled(this.guildId);
|
const suppressEveryone = UserGuildSettings.isEveryoneMentionSuppressed(this.guildId);
|
||||||
const suppressRoles = UserGuildSettings.isSuppressRolesEnabled(this.guildId);
|
const suppressRoles = UserGuildSettings.isRoleMentionSuppressed(this.guildId);
|
||||||
const mentions = message.mentions;
|
const mentions = message.mentions;
|
||||||
const mentionEveryone = 'mentionEveryone' in message ? message.mentionEveryone : message.mention_everyone;
|
const mentionEveryone = 'mentionEveryone' in message ? message.mentionEveryone : message.mention_everyone;
|
||||||
const mentionRoles = 'mentionRoles' in message ? message.mentionRoles : message.mention_roles;
|
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 invitableChannelId = InviteUtils.getInvitableChannelId(guild.id);
|
||||||
const canInvite = InviteUtils.canInviteToChannel(invitableChannelId, 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(() => {
|
const handleToggleHideMutedChannels = useCallback(() => {
|
||||||
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
||||||
}, [guild.id]);
|
}, [guild.id]);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface FavoritesGuildContextMenuProps {
|
|||||||
|
|
||||||
export const FavoritesGuildContextMenu: React.FC<FavoritesGuildContextMenuProps> = observer(({onClose}) => {
|
export const FavoritesGuildContextMenu: React.FC<FavoritesGuildContextMenuProps> = observer(({onClose}) => {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const settings = UserGuildSettings.getSettings(FAVORITES_GUILD_ID);
|
const settings = UserGuildSettings.getSettingsForScope(FAVORITES_GUILD_ID);
|
||||||
const isMuted = settings?.muted ?? false;
|
const isMuted = settings?.muted ?? false;
|
||||||
const handleToggleMute = () => {
|
const handleToggleMute = () => {
|
||||||
UserGuildSettingsCommands.updateGuildSettings(FAVORITES_GUILD_ID, {muted: !isMuted});
|
UserGuildSettingsCommands.updateGuildSettings(FAVORITES_GUILD_ID, {muted: !isMuted});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ interface GuildFolderContextMenuProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function guildHasUnreadState(guildId: string): boolean {
|
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}) => {
|
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,
|
(ch) => ch.parentId === category.id && ch.type !== ChannelTypes.GUILD_CATEGORY,
|
||||||
);
|
);
|
||||||
const hasUnread = channelsInCategory.some((ch) => ReadStates.hasUnread(ch.id));
|
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 categoryIds = channels.filter((ch) => ch.type === ChannelTypes.GUILD_CATEGORY).map((ch) => ch.id);
|
||||||
const allCategoriesCollapsed =
|
const allCategoriesCollapsed =
|
||||||
categoryIds.length > 0
|
categoryIds.length > 0
|
||||||
? categoryIds.every((categoryId) => UserGuildSettings.isChannelCollapsed(guildId, categoryId))
|
? categoryIds.every((categoryId) => UserGuildSettings.isChannelSectionCollapsed(guildId, categoryId))
|
||||||
: false;
|
: false;
|
||||||
const categoryOverride = UserGuildSettings.getChannelOverride(guildId, category.id);
|
const categoryOverride = UserGuildSettings.getChannelOverride(guildId, category.id);
|
||||||
const isMuted = categoryOverride?.muted ?? false;
|
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}) => {
|
export const CollapseCategoryMenuItem: React.FC<CategoryMenuItemProps> = observer(({category, onClose}) => {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const guildId = category.guildId!;
|
const guildId = category.guildId!;
|
||||||
const isCollapsed = UserGuildSettings.isChannelCollapsed(guildId, category.id);
|
const isCollapsed = UserGuildSettings.isChannelSectionCollapsed(guildId, category.id);
|
||||||
const handleToggleCollapse = useCallback(() => {
|
const handleToggleCollapse = useCallback(() => {
|
||||||
UserGuildSettingsCommands.toggleChannelCollapsed(guildId, category.id);
|
UserGuildSettingsCommands.toggleChannelCollapsed(guildId, category.id);
|
||||||
onClose();
|
onClose();
|
||||||
@@ -138,7 +138,7 @@ export const CollapseAllCategoriesMenuItem: React.FC<CategoryMenuItemProps> = ob
|
|||||||
);
|
);
|
||||||
const allCategoriesCollapsed = useMemo(() => {
|
const allCategoriesCollapsed = useMemo(() => {
|
||||||
if (categoryIds.length === 0) return false;
|
if (categoryIds.length === 0) return false;
|
||||||
return categoryIds.every((categoryId) => UserGuildSettings.isChannelCollapsed(guildId, categoryId));
|
return categoryIds.every((categoryId) => UserGuildSettings.isChannelSectionCollapsed(guildId, categoryId));
|
||||||
}, [guildId, categoryIds]);
|
}, [guildId, categoryIds]);
|
||||||
const handleToggleCollapseAll = useCallback(() => {
|
const handleToggleCollapseAll = useCallback(() => {
|
||||||
UserGuildSettingsCommands.toggleAllCategoriesCollapsed(guildId, categoryIds);
|
UserGuildSettingsCommands.toggleAllCategoriesCollapsed(guildId, categoryIds);
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export function useGuildMenuData(guild: Guild, options: UseGuildMenuDataOptions)
|
|||||||
const isOwner = guild.isOwner(Authentication.currentUserId);
|
const isOwner = guild.isOwner(Authentication.currentUserId);
|
||||||
const developerMode = UserSettings.developerMode;
|
const developerMode = UserSettings.developerMode;
|
||||||
const canEditCommunityProfile = Users.getCurrentUser()?.isClaimed() ?? true;
|
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 hideMutedChannels = settings?.hide_muted_channels ?? false;
|
||||||
const isMuted = settings?.muted ?? false;
|
const isMuted = settings?.muted ?? false;
|
||||||
const muteConfig = settings?.mute_config;
|
const muteConfig = settings?.mute_config;
|
||||||
@@ -298,7 +298,7 @@ export function useGuildMenuData(guild: Guild, options: UseGuildMenuDataOptions)
|
|||||||
ModalCommands.runAfterBottomSheetClose(onClose, () => openReportGuildModal({i18n, guild}));
|
ModalCommands.runAfterBottomSheetClose(onClose, () => openReportGuildModal({i18n, guild}));
|
||||||
},
|
},
|
||||||
handleToggleHideMutedChannels: (checked: boolean) => {
|
handleToggleHideMutedChannels: (checked: boolean) => {
|
||||||
const currentSettings = UserGuildSettings.getSettings(guild.id);
|
const currentSettings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||||
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
||||||
if (checked === currentValue) return;
|
if (checked === currentValue) return;
|
||||||
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
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}) => {
|
export const MuteCommunityMenuItem: React.FC<GuildMenuItemProps> = observer(({guild, onClose}) => {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const settings = UserGuildSettings.getSettings(guild.id);
|
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||||
const isMuted = settings?.muted ?? false;
|
const isMuted = settings?.muted ?? false;
|
||||||
const muteConfig = settings?.mute_config;
|
const muteConfig = settings?.mute_config;
|
||||||
const mutedText = getMutedText(isMuted, muteConfig);
|
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}) => {
|
export const NotificationSettingsMenuItem: React.FC<GuildMenuItemProps> = observer(({guild, onClose}) => {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const settings = UserGuildSettings.getSettings(guild.id);
|
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||||
const suppressEveryone = settings?.suppress_everyone ?? false;
|
const suppressEveryone = settings?.suppress_everyone ?? false;
|
||||||
const suppressRoles = settings?.suppress_roles ?? false;
|
const suppressRoles = settings?.suppress_roles ?? false;
|
||||||
const mobilePush = settings?.mobile_push ?? true;
|
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}) => {
|
export const HideMutedChannelsMenuItem: React.FC<GuildMenuItemProps> = observer(({guild}) => {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const settings = UserGuildSettings.getSettings(guild.id);
|
const settings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||||
const hideMutedChannels = settings?.hide_muted_channels ?? false;
|
const hideMutedChannels = settings?.hide_muted_channels ?? false;
|
||||||
const handleToggle = useCallback(
|
const handleToggle = useCallback(
|
||||||
(checked: boolean) => {
|
(checked: boolean) => {
|
||||||
const currentSettings = UserGuildSettings.getSettings(guild.id);
|
const currentSettings = UserGuildSettings.getSettingsForScope(guild.id);
|
||||||
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
const currentValue = currentSettings?.hide_muted_channels ?? false;
|
||||||
if (checked === currentValue) return;
|
if (checked === currentValue) return;
|
||||||
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
UserGuildSettingsCommands.toggleHideMutedChannels(guild.id);
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class NotificationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldNotifyBasedOnSettings(channel: Channel, messageRecord: Message, currentUser: User): boolean {
|
private shouldNotifyBasedOnSettings(channel: Channel, messageRecord: Message, currentUser: User): boolean {
|
||||||
const level = UserGuildSettings.resolvedMessageNotifications({
|
const level = UserGuildSettings.resolveEffectiveMessageNotifications({
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
guildId: channel.guildId,
|
guildId: channel.guildId,
|
||||||
parentId: channel.parentId ?? undefined,
|
parentId: channel.parentId ?? undefined,
|
||||||
@@ -261,7 +261,7 @@ class NotificationState {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
UserGuildSettings.allowNoMessages({
|
UserGuildSettings.resolvesToNoMessages({
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
guildId: channel.guildId,
|
guildId: channel.guildId,
|
||||||
parentId: channel.parentId ?? undefined,
|
parentId: channel.parentId ?? undefined,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function updateGuildSettings(
|
|||||||
updates: UserGuildSettingsPartial,
|
updates: UserGuildSettingsPartial,
|
||||||
options?: PersistenceOptions,
|
options?: PersistenceOptions,
|
||||||
): void {
|
): void {
|
||||||
UserGuildSettings.getSettings(guildId);
|
UserGuildSettings.getSettingsForScope(guildId);
|
||||||
UserGuildSettings.updateGuildSettings(guildId, updates as Partial<GatewayGuildSettings>);
|
UserGuildSettings.updateGuildSettings(guildId, updates as Partial<GatewayGuildSettings>);
|
||||||
scheduleUpdate(guildId, updates, options);
|
scheduleUpdate(guildId, updates, options);
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ export function bulkUpdateGuildSettings(
|
|||||||
const uniqueGuildIds = Array.from(new Set(guildIds));
|
const uniqueGuildIds = Array.from(new Set(guildIds));
|
||||||
if (uniqueGuildIds.length === 0) return;
|
if (uniqueGuildIds.length === 0) return;
|
||||||
for (const guildId of uniqueGuildIds) {
|
for (const guildId of uniqueGuildIds) {
|
||||||
UserGuildSettings.getSettings(guildId);
|
UserGuildSettings.getSettingsForScope(guildId);
|
||||||
UserGuildSettings.updateGuildSettings(guildId, updates as Partial<GatewayGuildSettings>);
|
UserGuildSettings.updateGuildSettings(guildId, updates as Partial<GatewayGuildSettings>);
|
||||||
const key = getUpdateKey(guildId);
|
const key = getUpdateKey(guildId);
|
||||||
addPendingPayload(key, updates);
|
addPendingPayload(key, updates);
|
||||||
@@ -148,7 +148,7 @@ export function bulkUpdateGuildSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toggleHideMutedChannels(guildId: string | null): void {
|
export function toggleHideMutedChannels(guildId: string | null): void {
|
||||||
const currentSettings = UserGuildSettings.getSettings(guildId);
|
const currentSettings = UserGuildSettings.getSettingsForScope(guildId);
|
||||||
const newValue = !currentSettings.hide_muted_channels;
|
const newValue = !currentSettings.hide_muted_channels;
|
||||||
updateGuildSettings(guildId, {hide_muted_channels: newValue});
|
updateGuildSettings(guildId, {hide_muted_channels: newValue});
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ export function updateChannelOverride(
|
|||||||
override: ChannelOverrideUpdate,
|
override: ChannelOverrideUpdate,
|
||||||
options?: PersistenceOptions,
|
options?: PersistenceOptions,
|
||||||
): void {
|
): void {
|
||||||
const currentSettings = UserGuildSettings.getSettings(guildId);
|
const currentSettings = UserGuildSettings.getSettingsForScope(guildId);
|
||||||
const currentOverride = UserGuildSettings.getChannelOverride(guildId, channelId);
|
const currentOverride = UserGuildSettings.getChannelOverride(guildId, channelId);
|
||||||
let newOverride: ChannelOverride | null = null;
|
let newOverride: ChannelOverride | null = null;
|
||||||
if (override != null) {
|
if (override != null) {
|
||||||
@@ -182,7 +182,7 @@ export function bulkUpdateChannelOverrides(
|
|||||||
): void {
|
): void {
|
||||||
const uniqueChannelIds = Array.from(new Set(channelIds));
|
const uniqueChannelIds = Array.from(new Set(channelIds));
|
||||||
if (uniqueChannelIds.length === 0) return;
|
if (uniqueChannelIds.length === 0) return;
|
||||||
const currentSettings = UserGuildSettings.getSettings(guildId);
|
const currentSettings = UserGuildSettings.getSettingsForScope(guildId);
|
||||||
const newChannelOverrides: Record<string, ChannelOverride> = {...(currentSettings.channel_overrides ?? {})};
|
const newChannelOverrides: Record<string, ChannelOverride> = {...(currentSettings.channel_overrides ?? {})};
|
||||||
for (const channelId of uniqueChannelIds) {
|
for (const channelId of uniqueChannelIds) {
|
||||||
const currentOverride = UserGuildSettings.getChannelOverride(guildId, channelId);
|
const currentOverride = UserGuildSettings.getChannelOverride(guildId, channelId);
|
||||||
@@ -196,7 +196,7 @@ export function bulkUpdateChannelOverrides(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toggleChannelCollapsed(guildId: string | null, channelId: string): void {
|
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});
|
updateChannelOverride(guildId, channelId, {collapsed: !isCollapsed});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ export function updateUnreadBadgesLevel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toggleChannelMuted(guildId: string | null, channelId: string, options?: PersistenceOptions): void {
|
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);
|
updateChannelOverride(guildId, channelId, {muted: !isMuted}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,13 +236,13 @@ export function toggleAllCategoriesCollapsed(guildId: string | null, categoryIds
|
|||||||
const uniqueCategoryIds = Array.from(new Set(categoryIds));
|
const uniqueCategoryIds = Array.from(new Set(categoryIds));
|
||||||
if (uniqueCategoryIds.length === 0) return;
|
if (uniqueCategoryIds.length === 0) return;
|
||||||
const allCollapsed = uniqueCategoryIds.every((categoryId) =>
|
const allCollapsed = uniqueCategoryIds.every((categoryId) =>
|
||||||
UserGuildSettings.isChannelCollapsed(guildId, categoryId),
|
UserGuildSettings.isChannelSectionCollapsed(guildId, categoryId),
|
||||||
);
|
);
|
||||||
const newCollapsedState = !allCollapsed;
|
const newCollapsedState = !allCollapsed;
|
||||||
for (const categoryId of uniqueCategoryIds) {
|
for (const categoryId of uniqueCategoryIds) {
|
||||||
UserGuildSettings.updateChannelOverride(guildId, categoryId, {collapsed: newCollapsedState});
|
UserGuildSettings.updateChannelOverride(guildId, categoryId, {collapsed: newCollapsedState});
|
||||||
}
|
}
|
||||||
const currentSettings = UserGuildSettings.getSettings(guildId);
|
const currentSettings = UserGuildSettings.getSettingsForScope(guildId);
|
||||||
const newChannelOverrides: Record<string, ChannelOverride> = {...(currentSettings.channel_overrides ?? {})};
|
const newChannelOverrides: Record<string, ChannelOverride> = {...(currentSettings.channel_overrides ?? {})};
|
||||||
for (const categoryId of uniqueCategoryIds) {
|
for (const categoryId of uniqueCategoryIds) {
|
||||||
const currentOverride = newChannelOverrides[categoryId];
|
const currentOverride = newChannelOverrides[categoryId];
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface ChannelOverride {
|
|||||||
unread_badges?: number | null;
|
unread_badges?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GuildSettings {
|
interface StoredGuildSettings {
|
||||||
guild_id: string;
|
guild_id: string;
|
||||||
suppress_everyone: boolean;
|
suppress_everyone: boolean;
|
||||||
suppress_roles: boolean;
|
suppress_roles: boolean;
|
||||||
@@ -63,7 +63,7 @@ export interface GatewayGuildSettings {
|
|||||||
unread_badges?: number | null;
|
unread_badges?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_GUILD_SETTINGS: Omit<GuildSettings, 'guild_id'> = {
|
const DEFAULT_GUILD_SETTINGS: Omit<StoredGuildSettings, 'guild_id'> = {
|
||||||
suppress_everyone: false,
|
suppress_everyone: false,
|
||||||
suppress_roles: false,
|
suppress_roles: false,
|
||||||
mute_scheduled_events: false,
|
mute_scheduled_events: false,
|
||||||
@@ -95,7 +95,7 @@ function parseEndTime(end_time?: string): number | null {
|
|||||||
return Number.isFinite(ms) ? ms : null;
|
return Number.isFinite(ms) ? ms : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMuted(config: {
|
function isMuteActive(config: {
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
mute_config?: {
|
mute_config?: {
|
||||||
end_time?: string;
|
end_time?: string;
|
||||||
@@ -114,8 +114,8 @@ interface UnreadInboxReadState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UserGuildSettings {
|
class UserGuildSettings {
|
||||||
private readonly guildSettings = observable.map<GuildId, GuildSettings>();
|
private readonly guildSettings = observable.map<GuildId, StoredGuildSettings>();
|
||||||
private readonly mutedChannels = observable.map<GuildId, Set<ChannelId>>();
|
private readonly mutedChannelsByGuild = observable.map<GuildId, Set<ChannelId>>();
|
||||||
updateCounter = 0;
|
updateCounter = 0;
|
||||||
private readonly pendingGuildUpdates = new Set<GuildId | null>();
|
private readonly pendingGuildUpdates = new Set<GuildId | null>();
|
||||||
private readonly guildMuteTimers = new Map<GuildId, NodeJS.Timeout>();
|
private readonly guildMuteTimers = new Map<GuildId, NodeJS.Timeout>();
|
||||||
@@ -149,7 +149,7 @@ class UserGuildSettings {
|
|||||||
this.pendingGuildUpdates.add(guildId as GuildId | null);
|
this.pendingGuildUpdates.add(guildId as GuildId | null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultSettings(guildId: string): GuildSettings {
|
private getDefaultSettings(guildId: string): StoredGuildSettings {
|
||||||
return {
|
return {
|
||||||
...DEFAULT_GUILD_SETTINGS,
|
...DEFAULT_GUILD_SETTINGS,
|
||||||
guild_id: guildId,
|
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);
|
const key = this.storageKeyFor(guildId);
|
||||||
return this.guildSettings.get(key as 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);
|
const key = this.storageKeyFor(guildId);
|
||||||
let settings = this.getStoredSettings(key);
|
let settings = this.getStoredSettings(key);
|
||||||
if (settings == null) {
|
if (settings == null) {
|
||||||
@@ -174,12 +174,12 @@ class UserGuildSettings {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuildSettings(guildId: string | null): GuildSettings {
|
getGuildSettings(guildId: string | null): StoredGuildSettings {
|
||||||
const key = this.storageKeyFor(guildId);
|
const key = this.storageKeyFor(guildId);
|
||||||
return this.getStoredSettings(key) ?? this.getDefaultSettings(key);
|
return this.getStoredSettings(key) ?? this.getDefaultSettings(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettings(guildId: string | null): GuildSettings {
|
getSettingsForScope(guildId: string | null): StoredGuildSettings {
|
||||||
return this.getGuildSettings(guildId);
|
return this.getGuildSettings(guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,17 +208,17 @@ class UserGuildSettings {
|
|||||||
return result as Record<ChannelId, ChannelOverride>;
|
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>();
|
const set = new Set<ChannelId>();
|
||||||
for (const [channelId, override] of Object.entries(settings.channel_overrides)) {
|
for (const [channelId, override] of Object.entries(settings.channel_overrides)) {
|
||||||
if (isMuted(override)) {
|
if (isMuteActive(override)) {
|
||||||
set.add(channelId as ChannelId);
|
set.add(channelId as ChannelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (set.size > 0) {
|
if (set.size > 0) {
|
||||||
this.mutedChannels.set(guildId as GuildId, set);
|
this.mutedChannelsByGuild.set(guildId as GuildId, set);
|
||||||
} else {
|
} 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.clearGuildMuteTimer(guildId);
|
||||||
this.clearGuildChannelTimers(guildId);
|
this.clearGuildChannelTimers(guildId);
|
||||||
if (settings.muted && settings.mute_config?.end_time) {
|
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;
|
if (!settings.muted) return settings;
|
||||||
const endMs = parseEndTime(settings.mute_config?.end_time);
|
const endMs = parseEndTime(settings.mute_config?.end_time);
|
||||||
if (endMs != null && endMs <= Date.now()) {
|
if (endMs != null && endMs <= Date.now()) {
|
||||||
@@ -296,7 +296,7 @@ class UserGuildSettings {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeChannelMutes(settings: GuildSettings): GuildSettings {
|
private sanitizeChannelMutes(settings: StoredGuildSettings): StoredGuildSettings {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const overrides: Record<string, ChannelOverride> = {...settings.channel_overrides};
|
const overrides: Record<string, ChannelOverride> = {...settings.channel_overrides};
|
||||||
for (const [channelId, override] of Object.entries(overrides)) {
|
for (const [channelId, override] of Object.entries(overrides)) {
|
||||||
@@ -314,7 +314,7 @@ class UserGuildSettings {
|
|||||||
const key = this.storageKeyFor(guildId);
|
const key = this.storageKeyFor(guildId);
|
||||||
const existing = this.ensureGuildSettings(key);
|
const existing = this.ensureGuildSettings(key);
|
||||||
const overridesInput = 'channel_overrides' in updates ? updates.channel_overrides : existing.channel_overrides;
|
const overridesInput = 'channel_overrides' in updates ? updates.channel_overrides : existing.channel_overrides;
|
||||||
const merged: GuildSettings = {
|
const merged: StoredGuildSettings = {
|
||||||
...existing,
|
...existing,
|
||||||
...updates,
|
...updates,
|
||||||
guild_id: key,
|
guild_id: key,
|
||||||
@@ -333,7 +333,7 @@ class UserGuildSettings {
|
|||||||
const settings = this.ensureGuildSettings(key);
|
const settings = this.ensureGuildSettings(key);
|
||||||
const existing = settings.channel_overrides[channelId as ChannelId] ?? DEFAULT_CHANNEL_OVERRIDE(channelId);
|
const existing = settings.channel_overrides[channelId as ChannelId] ?? DEFAULT_CHANNEL_OVERRIDE(channelId);
|
||||||
const updated = {...existing, ...updates, channel_id: channelId};
|
const updated = {...existing, ...updates, channel_id: channelId};
|
||||||
const next: GuildSettings = {
|
const next: StoredGuildSettings = {
|
||||||
...settings,
|
...settings,
|
||||||
channel_overrides: {...settings.channel_overrides, [channelId]: updated} as Record<ChannelId, ChannelOverride>,
|
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);
|
const existing = nextOverrides[channelId] ?? DEFAULT_CHANNEL_OVERRIDE(channelId);
|
||||||
nextOverrides[channelId] = {...existing, ...updates, channel_id: 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));
|
const sanitized = this.sanitizeChannelMutes(this.sanitizeGuildMute(next));
|
||||||
this.guildSettings.set(key as GuildId, sanitized);
|
this.guildSettings.set(key as GuildId, sanitized);
|
||||||
this.updateMutedChannelsCache(key, sanitized);
|
this.updateMutedChannelsCache(key, sanitized);
|
||||||
@@ -362,24 +365,24 @@ class UserGuildSettings {
|
|||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
isSuppressEveryoneEnabled(guildId: string | null): boolean {
|
isEveryoneMentionSuppressed(guildId: string | null): boolean {
|
||||||
return guildId != null && this.getGuildSettings(guildId).suppress_everyone;
|
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;
|
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;
|
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;
|
return guildId == null || this.getGuildSettings(guildId).mobile_push;
|
||||||
}
|
}
|
||||||
|
|
||||||
isMuted(guildId: string | null): boolean {
|
isWholeGuildMuted(guildId: string | null): boolean {
|
||||||
return isMuted(this.getGuildSettings(guildId));
|
return isMuteActive(this.getGuildSettings(guildId));
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuildMessageNotifications(guildId: string | null): number {
|
getGuildMessageNotifications(guildId: string | null): number {
|
||||||
@@ -401,7 +404,7 @@ class UserGuildSettings {
|
|||||||
return settings.message_notifications;
|
return settings.message_notifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuildIds(): Array<GuildId> {
|
getKnownGuildIds(): Array<GuildId> {
|
||||||
return Array.from(this.guildSettings.keys());
|
return Array.from(this.guildSettings.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +412,7 @@ class UserGuildSettings {
|
|||||||
return this.getGuildSettings(guildId).message_notifications;
|
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;
|
return this.getGuildSettings(guildId).channel_overrides;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,50 +420,59 @@ class UserGuildSettings {
|
|||||||
return this.getGuildSettings(guildId).channel_overrides[channelId as ChannelId];
|
return this.getGuildSettings(guildId).channel_overrides[channelId as ChannelId];
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelMessageNotifications(guildId: string | null, channelId: string): number {
|
getChannelNotificationLevel(guildId: string | null, channelId: string): number {
|
||||||
return (
|
return (
|
||||||
this.getGuildSettings(guildId).channel_overrides[channelId as ChannelId]?.message_notifications ??
|
this.getGuildSettings(guildId).channel_overrides[channelId as ChannelId]?.message_notifications ??
|
||||||
MessageNotifications.NULL
|
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];
|
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;
|
if (guildId == null) return false;
|
||||||
const channel = Channels.getChannel(channelId);
|
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 {
|
isMutedAtAnyLevel(guildId: string | null, channelId: string): boolean {
|
||||||
return this.isMuted(guildId) || this.isCategoryMuted(guildId, channelId) || this.isChannelMuted(guildId, channelId);
|
return (
|
||||||
|
this.isWholeGuildMuted(guildId) ||
|
||||||
|
this.isParentCategoryMuted(guildId, channelId) ||
|
||||||
|
this.isChannelDirectlyMuted(guildId, channelId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isGuildOrChannelMuted(guildId: string | null, channelId: string): boolean {
|
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 {
|
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> {
|
getMutedChannelIds(guildId: string): Set<ChannelId> {
|
||||||
return new Set(this.mutedChannels.get(guildId as GuildId) ?? []);
|
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;
|
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 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 (direct !== MessageNotifications.NULL && direct !== MessageNotifications.INHERIT) return direct;
|
||||||
if (channel.parentId != null && guildId != null) {
|
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;
|
if (parent !== MessageNotifications.NULL && parent !== MessageNotifications.INHERIT) return parent;
|
||||||
}
|
}
|
||||||
return this.getGuildMessageNotifications(guildId);
|
return this.getGuildMessageNotifications(guildId);
|
||||||
@@ -480,7 +492,7 @@ class UserGuildSettings {
|
|||||||
|
|
||||||
private getLockedCommunityUnreadBadgesLevel(guildId: string | null): number | null {
|
private getLockedCommunityUnreadBadgesLevel(guildId: string | null): number | null {
|
||||||
if (guildId == null) return 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);
|
const notificationLevel = this.getGuildMessageNotifications(guildId);
|
||||||
if (notificationLevel === MessageNotifications.ALL_MESSAGES) return MessageNotifications.ALL_MESSAGES;
|
if (notificationLevel === MessageNotifications.ALL_MESSAGES) return MessageNotifications.ALL_MESSAGES;
|
||||||
if (notificationLevel === MessageNotifications.NO_MESSAGES) return MessageNotifications.NO_MESSAGES;
|
if (notificationLevel === MessageNotifications.NO_MESSAGES) return MessageNotifications.NO_MESSAGES;
|
||||||
@@ -527,35 +539,35 @@ class UserGuildSettings {
|
|||||||
const guildId = channel.guildId ?? null;
|
const guildId = channel.guildId ?? null;
|
||||||
return resolveUnreadInboxVisibility({
|
return resolveUnreadInboxVisibility({
|
||||||
unreadBadgesLevel: this.resolvedUnreadBadgesLevel(channel),
|
unreadBadgesLevel: this.resolvedUnreadBadgesLevel(channel),
|
||||||
isMuted: this.isGuildOrCategoryOrChannelMuted(guildId, channel.id),
|
isMuted: this.isMutedAtAnyLevel(guildId, channel.id),
|
||||||
hasUnread: readState.hasUnread,
|
hasUnread: readState.hasUnread,
|
||||||
hasMentions: readState.hasMentions,
|
hasMentions: readState.hasMentions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveUnreadSetting(channel: {id: string; guildId?: string; parentId?: string; type: number}): string {
|
getUnreadSettingKey(channel: {id: string; guildId?: string; parentId?: string; type: number}): string {
|
||||||
const level = this.resolvedMessageNotifications(channel);
|
const level = this.resolveEffectiveMessageNotifications(channel);
|
||||||
return level === MessageNotifications.ALL_MESSAGES ? 'all_messages' : 'only_mentions';
|
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 (
|
return (
|
||||||
this.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) ||
|
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 (
|
return (
|
||||||
!this.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) &&
|
!this.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) &&
|
||||||
this.resolvedMessageNotifications(channel) === MessageNotifications.ALL_MESSAGES
|
this.resolveEffectiveMessageNotifications(channel) === MessageNotifications.ALL_MESSAGES
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnectionOpen(userGuildSettings: Array<GatewayGuildSettings>): void {
|
handleConnectionOpen(userGuildSettings: Array<GatewayGuildSettings>): void {
|
||||||
this.clearAllTimers();
|
this.clearAllTimers();
|
||||||
this.guildSettings.clear();
|
this.guildSettings.clear();
|
||||||
this.mutedChannels.clear();
|
this.mutedChannelsByGuild.clear();
|
||||||
for (const settings of userGuildSettings) {
|
for (const settings of userGuildSettings) {
|
||||||
this.updateGuildSettings(settings.guild_id, settings);
|
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 (
|
if (
|
||||||
UserGuildSettings.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) ||
|
UserGuildSettings.isGuildOrChannelMuted(channel.guildId ?? null, channel.id) ||
|
||||||
UserGuildSettings.isCategoryMuted(channel.guildId ?? null, channel.id)
|
UserGuildSettings.isParentCategoryMuted(channel.guildId ?? null, channel.id)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Notification from '@app/features/ui/state/Notification';
|
|||||||
import Window from '@app/features/window/state/Window';
|
import Window from '@app/features/window/state/Window';
|
||||||
|
|
||||||
function applyFocusSideEffects(focused: boolean): void {
|
function applyFocusSideEffects(focused: boolean): void {
|
||||||
GuildReadState.handleWindowFocus();
|
GuildReadState.handleWindowFocused();
|
||||||
Notification.handleWindowFocus({focused});
|
Notification.handleWindowFocus({focused});
|
||||||
if (focused) {
|
if (focused) {
|
||||||
Idle.recordActivity();
|
Idle.recordActivity();
|
||||||
|
|||||||
Reference in new issue
Block a user