/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {makeAutoObservable} from 'mobx'; import {makePersistent} from '~/lib/MobXPersistence'; import ChannelStore from './ChannelStore'; import UserGuildSettingsStore from './UserGuildSettingsStore'; export interface FavoriteChannel { channelId: string; guildId: string; parentId: string | null; position: number; nickname: string | null; } export interface FavoriteCategory { id: string; name: string; position: number; } class FavoritesStore { channels: Array = []; categories: Array = []; collapsedCategories = new Set(); hideMutedChannels: boolean = false; isMuted: boolean = false; constructor() { makeAutoObservable(this, {}, {autoBind: true}); void this.initPersistence(); } private async initPersistence(): Promise { await makePersistent(this, 'FavoritesStore', [ 'channels', 'categories', 'collapsedCategories', 'hideMutedChannels', 'isMuted', ]); } get hasAnyFavorites(): boolean { return this.channels.length > 0; } get sortedCategories(): ReadonlyArray { return [...this.categories].sort((a, b) => a.position - b.position); } get sortedChannels(): ReadonlyArray { return [...this.channels].sort((a, b) => a.position - b.position); } getChannel(channelId: string): FavoriteChannel | undefined { return this.channels.find((ch) => ch.channelId === channelId); } getCategory(categoryId: string): FavoriteCategory | undefined { return this.categories.find((cat) => cat.id === categoryId); } getChannelsInCategory(categoryId: string | null): ReadonlyArray { return this.sortedChannels.filter((ch) => ch.parentId === categoryId); } isCategoryCollapsed(categoryId: string): boolean { return this.collapsedCategories.has(categoryId); } getFirstAccessibleChannel(): FavoriteChannel | undefined { for (const fav of this.sortedChannels) { const channel = ChannelStore.getChannel(fav.channelId); if (!channel) continue; if (this.hideMutedChannels && channel.guildId) { if (UserGuildSettingsStore.isGuildOrChannelMuted(channel.guildId, channel.id)) { continue; } } return fav; } return undefined; } isChannelAccessible(channelId: string): boolean { const fav = this.getChannel(channelId); if (!fav) return false; const channel = ChannelStore.getChannel(fav.channelId); if (!channel) return false; if (this.hideMutedChannels && channel.guildId) { if (UserGuildSettingsStore.isGuildOrChannelMuted(channel.guildId, channel.id)) { return false; } } return true; } addChannel(channelId: string, guildId: string, parentId: string | null = null): void { const existing = this.channels.find((ch) => ch.channelId === channelId); if (existing) return; const position = this.channels.length; this.channels.push({ channelId, guildId, parentId, position, nickname: null, }); } addChannels(channelIds: Array, guildId: string, parentId: string | null = null): void { for (const channelId of channelIds) { this.addChannel(channelId, guildId, parentId); } } removeChannel(channelId: string): void { const index = this.channels.findIndex((ch) => ch.channelId === channelId); if (index === -1) return; this.channels.splice(index, 1); this.reorderChannels(); } setChannelNickname(channelId: string, nickname: string | null): void { const channel = this.channels.find((ch) => ch.channelId === channelId); if (!channel) return; channel.nickname = nickname; } moveChannel(channelId: string, newParentId: string | null, newIndexInCategory: number): void { const channel = this.channels.find((ch) => ch.channelId === channelId); if (!channel) return; const sorted = [...this.sortedChannels]; const currentIndex = sorted.findIndex((ch) => ch.channelId === channelId); if (currentIndex === -1) return; sorted.splice(currentIndex, 1); channel.parentId = newParentId; const categoryChannels = sorted.filter((ch) => ch.parentId === newParentId); if (newIndexInCategory >= categoryChannels.length) { const lastInCat = categoryChannels[categoryChannels.length - 1]; if (lastInCat) { const lastIndex = sorted.indexOf(lastInCat); sorted.splice(lastIndex + 1, 0, channel); } else { sorted.push(channel); } } else { const targetChannel = categoryChannels[newIndexInCategory]; const targetIndex = sorted.indexOf(targetChannel); sorted.splice(targetIndex, 0, channel); } this.channels = sorted; this.reorderChannels(); } createCategory(name: string): string { const id = `favorite-category-${Date.now()}`; const position = this.categories.length; this.categories.push({id, name, position}); return id; } renameCategory(categoryId: string, name: string): void { const category = this.categories.find((cat) => cat.id === categoryId); if (!category) return; category.name = name; } removeCategory(categoryId: string): void { const index = this.categories.findIndex((cat) => cat.id === categoryId); if (index === -1) return; this.categories.splice(index, 1); for (const channel of this.channels) { if (channel.parentId === categoryId) { channel.parentId = null; } } this.collapsedCategories.delete(categoryId); this.reorderCategories(); } toggleCategoryCollapsed(categoryId: string): void { if (this.collapsedCategories.has(categoryId)) { this.collapsedCategories.delete(categoryId); } else { this.collapsedCategories.add(categoryId); } } setHideMutedChannels(value: boolean): void { this.hideMutedChannels = value; } toggleMuted(): void { this.isMuted = !this.isMuted; } private reorderChannels(): void { this.channels = this.channels.map((ch, index) => ({ ...ch, position: index, })); } private reorderCategories(): void { this.categories = this.categories.map((cat, index) => ({ ...cat, position: index, })); } } export default new FavoritesStore();