initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {action, makeAutoObservable} from 'mobx';
import {ChannelTypes, ME} from '~/Constants';
import {Routes} from '~/Routes';
import {type Channel, ChannelRecord} from '~/records/ChannelRecord';
import type {GuildReadyData} from '~/records/GuildRecord';
import type {Message} from '~/records/MessageRecord';
import type {UserPartial} from '~/records/UserRecord';
import AuthenticationStore from '~/stores/AuthenticationStore';
import ChannelDisplayNameStore from '~/stores/ChannelDisplayNameStore';
import UserStore from '~/stores/UserStore';
import * as ChannelUtils from '~/utils/ChannelUtils';
import * as RouterUtils from '~/utils/RouterUtils';
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';
const sortDMs = (a: ChannelRecord, b: ChannelRecord) => {
const aTimestamp = a.lastMessageId ? SnowflakeUtils.extractTimestamp(a.lastMessageId) : null;
const bTimestamp = b.lastMessageId ? SnowflakeUtils.extractTimestamp(b.lastMessageId) : null;
if (aTimestamp != null && bTimestamp != null) {
return bTimestamp - aTimestamp;
}
if (aTimestamp != null) return -1;
if (bTimestamp != null) return 1;
return b.createdAt.getTime() - a.createdAt.getTime();
};
class ChannelStore {
private readonly channelsById = new Map<string, ChannelRecord>();
private readonly optimisticChannelBackups = new Map<string, ChannelRecord>();
constructor() {
makeAutoObservable(this, {}, {autoBind: true});
}
get channels(): ReadonlyArray<ChannelRecord> {
return Array.from(this.channelsById.values());
}
get allChannels(): ReadonlyArray<ChannelRecord> {
return this.channels;
}
get dmChannels(): ReadonlyArray<ChannelRecord> {
return this.channels
.filter(
(channel) => !channel.guildId && (channel.type === ChannelTypes.DM || channel.type === ChannelTypes.GROUP_DM),
)
.sort(sortDMs);
}
getChannel(channelId: string): ChannelRecord | undefined {
return this.channelsById.get(channelId);
}
getGuildChannels(guildId: string): ReadonlyArray<ChannelRecord> {
return this.channels.filter((channel) => channel.guildId === guildId).sort(ChannelUtils.compareChannels);
}
getPrivateChannels(): ReadonlyArray<ChannelRecord> {
return this.channels.filter(
(channel) => !channel.guildId && (channel.type === ChannelTypes.DM || channel.type === ChannelTypes.GROUP_DM),
);
}
@action
removeChannelOptimistically(channelId: string): void {
if (this.optimisticChannelBackups.has(channelId)) {
return;
}
const channel = this.channelsById.get(channelId);
if (!channel) {
return;
}
this.optimisticChannelBackups.set(channelId, channel);
this.channelsById.delete(channelId);
ChannelDisplayNameStore.removeChannel(channelId);
}
@action
rollbackChannelDeletion(channelId: string): void {
const channel = this.optimisticChannelBackups.get(channelId);
if (!channel) {
return;
}
this.setChannel(channel);
this.optimisticChannelBackups.delete(channelId);
}
@action
clearOptimisticallyRemovedChannel(channelId: string): void {
this.optimisticChannelBackups.delete(channelId);
}
@action
private setChannel(channel: ChannelRecord | Channel): void {
const record = channel instanceof ChannelRecord ? channel : new ChannelRecord(channel);
this.channelsById.set(record.id, record);
ChannelDisplayNameStore.syncChannel(record);
}
@action
handleConnectionOpen({channels}: {channels: ReadonlyArray<Channel>}): void {
this.channelsById.clear();
ChannelDisplayNameStore.clear();
const allRecipients = channels
.filter((channel) => channel.recipients && channel.recipients.length > 0)
.flatMap((channel) => channel.recipients!);
if (allRecipients.length > 0) {
UserStore.cacheUsers(allRecipients);
}
for (const channel of channels) {
this.setChannel(channel);
}
const userId = AuthenticationStore.currentUserId;
if (!userId) {
return;
}
const personalNotesChannel: Channel = {
id: userId,
type: ChannelTypes.DM_PERSONAL_NOTES,
name: undefined,
topic: null,
url: null,
last_message_id: null,
last_pin_timestamp: null,
recipients: undefined,
parent_id: null,
bitrate: null,
user_limit: null,
};
this.setChannel(personalNotesChannel);
}
@action
handleGuildCreate(guild: GuildReadyData): void {
if (guild.unavailable) {
return;
}
for (const channel of guild.channels) {
this.setChannel(channel);
}
}
@action
handleGuildDelete({guildId}: {guildId: string}): void {
for (const [channelId, channel] of Array.from(this.channelsById.entries())) {
if (channel.guildId === guildId) {
this.channelsById.delete(channelId);
}
}
}
@action
handleChannelCreate({channel}: {channel: Channel}): void {
this.setChannel(channel);
}
@action
handleChannelUpdateBulk({channels}: {channels: Array<Channel>}): void {
for (const channel of channels) {
this.setChannel(channel);
}
}
@action
handleChannelPinsUpdate({channelId, lastPinTimestamp}: {channelId: string; lastPinTimestamp: string}): void {
const channel = this.channelsById.get(channelId);
if (!channel) {
return;
}
this.setChannel(
new ChannelRecord({
...channel.toJSON(),
last_pin_timestamp: lastPinTimestamp,
}),
);
}
@action
handleChannelRecipientAdd({channelId, user}: {channelId: string; user: UserPartial}): void {
const channel = this.channelsById.get(channelId);
if (!channel) {
return;
}
UserStore.cacheUsers([user]);
const newRecipients = [...channel.recipientIds, user.id];
this.setChannel(
channel.withUpdates({
recipients: newRecipients.map((id) => UserStore.getUser(id)!.toJSON()),
}),
);
}
@action
handleChannelRecipientRemove({channelId, user}: {channelId: string; user: UserPartial}): void {
const channel = this.channelsById.get(channelId);
if (!channel) {
return;
}
if (user.id === AuthenticationStore.currentUserId) {
this.channelsById.delete(channelId);
ChannelDisplayNameStore.removeChannel(channelId);
const history = RouterUtils.getHistory();
const currentPath = history?.location.pathname ?? '';
const expectedPath = Routes.dmChannel(channelId);
if (currentPath.startsWith(expectedPath)) {
RouterUtils.transitionTo(Routes.ME);
}
return;
}
const newRecipients = channel.recipientIds.filter((id) => id !== user.id);
this.setChannel(
channel.withUpdates({
recipients: newRecipients.map((id) => UserStore.getUser(id)!.toJSON()),
}),
);
}
@action
handleChannelDelete({channel}: {channel: Channel}): void {
this.clearOptimisticallyRemovedChannel(channel.id);
this.channelsById.delete(channel.id);
ChannelDisplayNameStore.removeChannel(channel.id);
const history = RouterUtils.getHistory();
const currentPath = history?.location.pathname ?? '';
const guildId = channel.guild_id ?? ME;
const expectedPath = guildId === ME ? Routes.dmChannel(channel.id) : Routes.guildChannel(guildId, channel.id);
if (!currentPath.startsWith(expectedPath)) {
return;
}
if (guildId === ME) {
RouterUtils.transitionTo(Routes.ME);
} else {
const guildChannels = this.getGuildChannels(guildId);
const selectableChannel = guildChannels.find((c) => c.type !== ChannelTypes.GUILD_CATEGORY);
if (selectableChannel) {
RouterUtils.transitionTo(Routes.guildChannel(guildId, selectableChannel.id));
} else {
RouterUtils.transitionTo(Routes.ME);
}
}
}
@action
handleMessageCreate({message}: {message: Message}): void {
const channel = this.channelsById.get(message.channel_id);
if (!channel) {
return;
}
this.setChannel(
new ChannelRecord({
...channel.toJSON(),
last_message_id: message.id,
}),
);
}
@action
handleGuildRoleDelete({guildId, roleId}: {guildId: string; roleId: string}): void {
for (const [, channel] of this.channelsById) {
if (channel.guildId !== guildId) {
continue;
}
if (!(roleId in channel.permissionOverwrites)) {
continue;
}
const filteredOverwrites = Object.entries(channel.permissionOverwrites)
.filter(([id]) => id !== roleId)
.map(([, overwrite]) => overwrite.toJSON());
this.setChannel(
new ChannelRecord({
...channel.toJSON(),
permission_overwrites: filteredOverwrites,
}),
);
}
}
}
export default new ChannelStore();