waffle/frontend/src/stores.js

351 lines
10 KiB
JavaScript
Raw Normal View History

import gateway, { GatewayEventType } from "./gateway";
2022-05-05 20:52:35 +03:00
import logger from "./logging";
import request from "./request";
2022-05-06 01:55:21 +03:00
import { apiRoute, getItem, setItem } from "./storage";
2022-05-05 20:52:35 +03:00
const storeLog = logger("Store");
2022-04-23 23:07:46 +03:00
class Store {
constructor(value=null, name="[no name]") {
this._handlers = [];
this.value = value;
2022-05-05 15:33:22 +03:00
this.name = name;
}
// like subscribe, but without initially calling the handler
2022-05-07 03:27:41 +03:00
watch(handler) {
const handlerIndex = this._handlers.push(handler) - 1;
storeLog(`(${this.name}) (watch/handler) New handler`, this.value);
2022-05-07 03:27:41 +03:00
return () => {
this._handlers.splice(handlerIndex, 1);
};
}
subscribe(handler) {
const handlerIndex = this._handlers.push(handler) - 1;
storeLog(`(${this.name}) (subscribe/initial) Calling handler`, this.value);
handler(this.value);
return () => {
this._handlers.splice(handlerIndex, 1);
};
}
2022-04-28 18:40:38 +03:00
set(value) {
if (value === this.value)
return;
2022-04-28 18:40:38 +03:00
this.value = value;
this.updated();
}
updated() {
storeLog(`(${this.name}) (updated) Calling all (${this._handlers.length}) handlers`, this.value);
for (let i = this._handlers.length - 1; i >= 0; i--) {
this._handlers[i](this.value);
}
}
}
class StorageItemStore extends Store {
constructor(key) {
super(getItem(key), `StorageItemStore[key=${key}]`);
this.watch(e => setItem(key, e));
}
}
class ChannelsStore extends Store {
constructor() {
2022-05-05 15:33:22 +03:00
super(gateway.channels || [], "ChannelsStore");
gateway.subscribe(GatewayEventType.Ready, ({ channels }) => {
this.value = channels;
if (channels.length >= 1) {
2022-05-06 01:55:21 +03:00
if (!selectedChannel.value || selectedChannel.value.id === -1) {
selectedChannel.set(channels[0]);
} else {
// if a channel id is already selected, we'll populate it with the data we just got from the gateway
const index = this.value.findIndex(e => e.id === selectedChannel.value.id);
if (index !== -1)
selectedChannel.set(this.value[index]);
else // if the channel doesn't exist, just select the first one
selectedChannel.set(channels[0]);
2022-05-06 01:55:21 +03:00
}
}
this.updated();
});
gateway.subscribe(GatewayEventType.ChannelCreate, (channel) => {
this.value.push(channel);
this.updated();
});
gateway.subscribe(GatewayEventType.ChannelDelete, ({ id }) => {
const index = this.value.findIndex(e => e.id === id);
if (index === -1)
return;
this.value.splice(index, 1);
this.updated();
});
gateway.subscribe(GatewayEventType.ChannelUpdate, (data) => {
const index = this.value.findIndex(e => e.id === data.id);
if (index === -1)
return;
if (!this.value[index])
return;
this.value[index] = data;
this.updated();
});
2022-04-28 18:40:38 +03:00
gateway.subscribe(GatewayEventType.MessageCreate, ({ channel_id }) => {
const index = this.value.findIndex(e => e.id === channel_id);
2022-04-28 18:48:44 +03:00
if (index === -1 || !this.value[index] || selectedChannel.value.id === channel_id)
2022-04-28 18:40:38 +03:00
return;
this.value[index]._hasUnreads = true;
this.updated();
});
selectedChannel.subscribe(({ id }) => {
const index = this.value.findIndex(e => e.id === id);
2022-04-28 18:48:44 +03:00
if (index === -1 || !this.value[index] || !this.value[index]._hasUnreads)
2022-04-28 18:40:38 +03:00
return;
this.value[index]._hasUnreads = false;
this.updated();
});
}
}
class GatewayStatusStore extends Store {
constructor() {
2022-05-05 15:33:22 +03:00
super({ open: gateway.open, ready: gateway.authenticated }, "GatewayStatusStore");
gateway.subscribe(GatewayEventType.Open, () => {
this.value.open = true;
this.updated();
});
gateway.subscribe(GatewayEventType.Close, () => {
this.value.open = false;
this.value.ready = false;
this.updated();
});
gateway.subscribe(GatewayEventType.Ready, () => {
this.value.ready = true;
this.updated();
});
}
}
2022-04-20 03:44:48 +03:00
class UserInfoStore extends Store {
constructor() {
2022-05-05 15:33:22 +03:00
super(null, "UserInfoStore");
2022-04-20 03:44:48 +03:00
gateway.subscribe(GatewayEventType.Ready, ({ user }) => {
this.value = user;
this.updated();
});
}
}
class MessageStore extends Store {
constructor(channelId) {
2022-05-05 15:33:22 +03:00
super([], `MessageStore[channelId=${channelId}]`);
this.channelId = channelId;
this.isCollectingOldMessages = true;
this.didDoInitialLoad = false;
}
setMessage(id, message) {
const index = this.value.findIndex(e => e.id === id);
if (index === -1)
return;
this.value[index] = message;
this.updated();
}
addMessage(message) {
this.value.push(message);
// only dispatch update if collectOldMessages didn't
if (!this.collectOldMessages()) {
this.updated();
}
}
2022-04-20 03:44:48 +03:00
updateId(oldId, newId) {
const index = this.value.findIndex(e => e.id === oldId);
if (index === -1)
return;
this.value[index].id = newId;
this.updated();
}
updateMessage(message) {
const index = this.value.findIndex(e => e.id === message.id);
if (index === -1)
return;
this.value[index] = message;
this.updated();
}
deleteMessage({ id }) {
const index = this.value.findIndex(e => e.id === id);
if (index === -1)
return;
this.value.splice(index, 1);
this.updated();
}
collectOldMessages() {
if (!this.isCollectingOldMessages)
return false;
const target = 50;
const delta = this.value.length - target;
if (delta >= 1) {
this.value.splice(0, delta);
this.updated();
return true;
} else {
return false;
}
}
setIsCollectingOldMessages(isCollectingOldMessages) {
this.isCollectingOldMessages = isCollectingOldMessages;
this.collectOldMessages();
}
async loadOlderMessages(beforeCommitToStore=null) {
if (this.channelId === -1)
return;
const oldestMessage = this.value[0];
const endpoint = oldestMessage ? `channels/${this.channelId}/messages/?before=${oldestMessage.id}` : `channels/${this.channelId}/messages`;
const res = await request("GET", apiRoute(endpoint), true, null);
if (res.success && res.ok && res.json) {
if (res.json.length < 1)
return;
if (beforeCommitToStore)
beforeCommitToStore(res.json);
res.json.reverse();
this.value = res.json.concat(this.value);
this.updated();
} else {
overlayStore.open("toast", {
message: "Messages failed to load"
});
}
}
async doInitialLoad() {
2022-04-27 22:03:51 +03:00
if (this.channelId === -1)
return;
await this.loadOlderMessages();
this.didDoInitialLoad = true;
}
}
class MessagesStoreProvider {
constructor() {
this.storeByChannel = new Map();
gateway.subscribe(GatewayEventType.MessageCreate, (message) => {
2022-04-20 03:44:48 +03:00
// we currently don't care about our own messages
if (gateway.user && message.author_id === gateway.user.id)
return;
const store = this.getStoreOrNull(message.channel_id);
if (store)
store.addMessage(message);
});
gateway.subscribe(GatewayEventType.MessageUpdate, (message) => {
const store = this.getStoreOrNull(message.channel_id);
if (store)
store.updateMessage(message);
});
gateway.subscribe(GatewayEventType.MessageDelete, (message) => {
const store = this.getStoreOrNull(message.channel_id);
if (store)
store.deleteMessage(message);
});
}
getStoreOrNull(channelId) {
return this.storeByChannel.get(channelId);
}
getStore(channelId) {
if (!this.storeByChannel.get(channelId)) {
2022-04-20 03:44:48 +03:00
const store = new MessageStore(channelId);
store.doInitialLoad();
this.storeByChannel.set(channelId, store);
}
return this.storeByChannel.get(channelId);
}
}
class OverlayStore extends Store {
constructor() {
super({
2022-04-26 03:01:21 +03:00
createChannel: null,
2022-04-26 22:45:40 +03:00
editChannel: null,
toast: null,
login: null,
2022-04-27 22:03:51 +03:00
createAccount: null,
settings: null
2022-05-05 15:33:22 +03:00
}, "OverlayStore");
}
2022-04-25 23:02:15 +03:00
open(name, props={}) {
this.value[name] = props;
this.updated();
}
close(name) {
if (!this.value[name])
return;
2022-04-25 23:02:15 +03:00
this.value[name] = null;
this.updated();
}
}
2022-05-06 01:55:21 +03:00
export const selectedChannel = new Store({ id: getItem("app:cache:openChannelId"), name: "none", creator_id: -1 }, "selectedChannel");
export const showSidebar = new Store(true, "showSidebar");
2022-05-05 15:33:22 +03:00
export const smallViewport = new Store(false, "smallViewport");
export const showChannelView = new Store(true, "showChannelView");
export const theme = new StorageItemStore("app:visual:theme");
2022-05-07 03:45:09 +03:00
export const doAnimations = new StorageItemStore("app:behavior:doAnimations");
export const channels = new ChannelsStore();
export const gatewayStatus = new GatewayStatusStore();
export const messagesStoreProvider = new MessagesStoreProvider();
2022-04-20 03:44:48 +03:00
export const userInfoStore = new UserInfoStore();
export const overlayStore = new OverlayStore();
2022-05-07 03:27:41 +03:00
2022-05-05 21:49:25 +03:00
export const allStores = {
selectedChannel,
showSidebar,
showChannelView,
smallViewport,
2022-05-07 03:27:41 +03:00
theme,
2022-05-07 03:45:09 +03:00
doAnimations,
2022-05-05 21:49:25 +03:00
channels,
gatewayStatus,
messagesStoreProvider,
userInfoStore,
overlayStore,
};
2022-05-06 01:55:21 +03:00
2022-05-07 03:27:41 +03:00
selectedChannel.watch((newSelectedChannel) => {
2022-05-06 01:55:21 +03:00
setItem("app:cache:openChannelId", newSelectedChannel.id);
});