for-legacy-web/src/mobx/index.ts

444 lines
13 KiB
TypeScript

import isEqual from "lodash.isequal";
import {
makeAutoObservable,
observable,
autorun,
runInAction,
reaction,
makeObservable,
action,
extendObservable,
} from "mobx";
import { Client } from "revolt.js";
import {
Attachment,
Channels,
Servers,
Users,
} from "revolt.js/dist/api/objects";
import {
RemoveChannelField,
RemoveMemberField,
RemoveServerField,
RemoveUserField,
} from "revolt.js/dist/api/routes";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
type Nullable<T> = T | null;
function toNullable<T>(data?: T) {
return typeof data === "undefined" ? null : data;
}
export class User {
_id: string;
username: string;
avatar: Nullable<Attachment>;
badges: Nullable<number>;
status: Nullable<Users.Status>;
relationship: Nullable<Users.Relationship>;
online: Nullable<boolean>;
constructor(data: Users.User) {
this._id = data._id;
this.username = data.username;
this.avatar = toNullable(data.avatar);
this.badges = toNullable(data.badges);
this.status = toNullable(data.status);
this.relationship = toNullable(data.relationship);
this.online = toNullable(data.online);
makeAutoObservable(this);
}
@action update(data: Partial<Users.User>, clear?: RemoveUserField) {
const apply = (key: string) => {
// This code has been tested.
// @ts-expect-error
if (data[key] && !isEqual(this[key], data[key])) {
// @ts-expect-error
this[key] = data[key];
}
};
switch (clear) {
case "Avatar":
this.avatar = null;
break;
case "StatusText": {
if (this.status) {
this.status.text = undefined;
}
}
}
apply("username");
apply("avatar");
apply("badges");
apply("status");
apply("relationship");
apply("online");
}
@action setRelationship(relationship: Users.Relationship) {
this.relationship = relationship;
}
}
export class Channel {
_id: string;
channel_type: Channels.Channel["channel_type"];
// Direct Message
active: Nullable<boolean> = null;
// Group
owner: Nullable<string> = null;
// Server
server: Nullable<string> = null;
// Permissions
permissions: Nullable<number> = null;
default_permissions: Nullable<number> = null;
role_permissions: Nullable<{ [key: string]: number }> = null;
// Common
name: Nullable<string> = null;
icon: Nullable<Attachment> = null;
description: Nullable<string> = null;
recipients: Nullable<string[]> = null;
last_message: Nullable<string | Channels.LastMessage> = null;
constructor(data: Channels.Channel) {
this._id = data._id;
this.channel_type = data.channel_type;
switch (data.channel_type) {
case "DirectMessage": {
this.active = toNullable(data.active);
this.recipients = toNullable(data.recipients);
this.last_message = toNullable(data.last_message);
break;
}
case "Group": {
this.recipients = toNullable(data.recipients);
this.name = toNullable(data.name);
this.owner = toNullable(data.owner);
this.description = toNullable(data.description);
this.last_message = toNullable(data.last_message);
this.icon = toNullable(data.icon);
this.permissions = toNullable(data.permissions);
break;
}
case "TextChannel":
case "VoiceChannel": {
this.server = toNullable(data.server);
this.name = toNullable(data.name);
this.description = toNullable(data.description);
this.icon = toNullable(data.icon);
this.default_permissions = toNullable(data.default_permissions);
this.role_permissions = toNullable(data.role_permissions);
if (data.channel_type === "TextChannel") {
this.last_message = toNullable(data.last_message);
}
break;
}
}
makeAutoObservable(this);
}
@action update(
data: Partial<Channels.Channel>,
clear?: RemoveChannelField,
) {
const apply = (key: string) => {
// This code has been tested.
// @ts-expect-error
if (data[key] && !isEqual(this[key], data[key])) {
// @ts-expect-error
this[key] = data[key];
}
};
switch (clear) {
case "Description":
this.description = null;
break;
case "Icon":
this.icon = null;
break;
}
apply("active");
apply("owner");
apply("permissions");
apply("default_permissions");
apply("role_permissions");
apply("name");
apply("icon");
apply("description");
apply("recipients");
apply("last_message");
}
@action groupJoin(user: string) {
this.recipients?.push(user);
}
@action groupLeave(user: string) {
this.recipients = toNullable(
this.recipients?.filter((x) => x !== user),
);
}
}
export class Server {
_id: string;
owner: string;
name: string;
description: Nullable<string> = null;
channels: string[] = [];
categories: Nullable<Servers.Category[]> = null;
system_messages: Nullable<Servers.SystemMessageChannels> = null;
roles: Nullable<{ [key: string]: Servers.Role }> = null;
default_permissions: Servers.PermissionTuple;
icon: Nullable<Attachment> = null;
banner: Nullable<Attachment> = null;
constructor(data: Servers.Server) {
this._id = data._id;
this.owner = data.owner;
this.name = data.name;
this.description = toNullable(data.description);
this.channels = data.channels;
this.categories = toNullable(data.categories);
this.system_messages = toNullable(data.system_messages);
this.roles = toNullable(data.roles);
this.default_permissions = data.default_permissions;
this.icon = toNullable(data.icon);
this.banner = toNullable(data.banner);
makeAutoObservable(this);
}
@action update(data: Partial<Servers.Server>, clear?: RemoveServerField) {
const apply = (key: string) => {
// This code has been tested.
// @ts-expect-error
if (data[key] && !isEqual(this[key], data[key])) {
// @ts-expect-error
this[key] = data[key];
}
};
switch (clear) {
case "Banner":
this.banner = null;
break;
case "Description":
this.description = null;
break;
case "Icon":
this.icon = null;
break;
}
apply("owner");
apply("name");
apply("description");
apply("channels");
apply("categories");
apply("system_messages");
apply("roles");
apply("default_permissions");
apply("icon");
apply("banner");
}
}
export class Member {
_id: Servers.MemberCompositeKey;
nickname: Nullable<string> = null;
avatar: Nullable<Attachment> = null;
roles: Nullable<string[]> = null;
constructor(data: Servers.Member) {
this._id = data._id;
this.nickname = toNullable(data.nickname);
this.avatar = toNullable(data.avatar);
this.roles = toNullable(data.roles);
makeAutoObservable(this);
}
@action update(data: Partial<Servers.Member>, clear?: RemoveMemberField) {
const apply = (key: string) => {
// This code has been tested.
// @ts-expect-error
if (data[key] && !isEqual(this[key], data[key])) {
// @ts-expect-error
this[key] = data[key];
}
};
switch (clear) {
case "Nickname":
this.nickname = null;
break;
case "Avatar":
this.avatar = null;
break;
}
apply("nickname");
apply("avatar");
apply("roles");
}
}
export class DataStore {
client: Client;
@observable users = new Map<string, User>();
@observable channels = new Map<string, Channel>();
@observable servers = new Map<string, Server>();
@observable members = new Map<Servers.MemberCompositeKey, Member>();
constructor(client: Client) {
makeAutoObservable(this, undefined, { proxy: false });
this.client = client;
}
@action
async packet(packet: ClientboundNotification) {
switch (packet.type) {
case "Ready": {
for (let user of packet.users) {
this.users.set(user._id, new User(user));
}
for (let channel of packet.channels) {
this.channels.set(channel._id, new Channel(channel));
}
for (let server of packet.servers) {
this.servers.set(server._id, new Server(server));
}
break;
}
case "ChannelCreate": {
this.channels.set(packet._id, new Channel(packet));
break;
}
case "ChannelUpdate": {
this.channels.get(packet.id)?.update(packet.data, packet.clear);
break;
}
case "ChannelDelete": {
this.channels.delete(packet.id);
break;
}
case "ChannelGroupJoin": {
this.channels.get(packet.id)?.groupJoin(packet.user);
if (!this.users.has(packet.user)) {
let user = await this.client.users.fetch(packet.user);
this.users.set(packet.user, new User(user));
}
break;
}
case "ChannelGroupLeave": {
this.channels.get(packet.id)?.groupJoin(packet.user);
break;
}
case "UserUpdate": {
this.users.get(packet.id)?.update(packet.data, packet.clear);
break;
}
case "UserRelationship": {
if (!this.users.has(packet.user._id)) {
this.users.set(packet.user._id, new User(packet.user));
}
this.users.get(packet.user._id)?.setRelationship(packet.status);
break;
}
case "ServerUpdate": {
this.servers.get(packet.id)?.update(packet.data, packet.clear);
break;
}
case "ServerDelete": {
let server = this.servers.get(packet.id);
if (server) {
for (let channel of server.channels) {
this.channels.delete(channel);
}
}
this.servers.delete(packet.id);
break;
}
case "ServerMemberUpdate": {
this.members.get(packet.id)?.update(packet.data, packet.clear);
break;
}
case "ServerMemberJoin": {
const _id = { server: packet.id, user: packet.user };
this.members.set(_id, new Member({ _id }));
if (!this.servers.has(packet.id)) {
let server = await this.client.servers.fetch(packet.id);
this.servers.set(packet.id, new Server(server));
for (let id of server.channels) {
let channel = this.client.channels.get(id);
if (channel) {
this.channels.set(id, new Channel(channel));
}
}
}
if (!this.users.has(packet.user)) {
let user = await this.client.users.fetch(packet.user);
this.users.set(packet.user, new User(user));
}
break;
}
case "ServerMemberLeave": {
this.members.delete({ server: packet.id, user: packet.user });
if (packet.user === this.client.user!._id) {
await this.packet({ type: "ServerDelete", id: packet.id });
}
break;
}
}
}
async fetchMembers(server: string) {
let res = await this.client.members.fetchMembers(server);
for (let user of res.users) {
if (!this.users.has(user._id)) {
this.users.set(user._id, new User(user));
}
}
return res.members;
}
}