import $ from 'jquery'; import app from 'state'; import { WebsocketMessageNames, WebsocketNamespaces } from 'types'; import {io, Socket} from 'socket.io-client'; import _ from 'lodash'; import m from 'mithril'; export enum ChatErrors { NOT_LOGGED_IN='User must be logged in to load chat' } export interface IChannel { id: number, name: string, category: string, chain_id: string, created_at: string, updated_at: string, unread: number, ChatMessages?: any[] } export class ChatNamespace { private chatNs: Socket; private _isConnected = false; private _initialized = false; public channels: Record<string, IChannel> = {}; public activeChannel: string; constructor() {} public async init() { this.chatNs = io(`/${WebsocketNamespaces.Chat}`, { transports: ['websocket'], }); this.chatNs.on('connect', this.onConnect.bind(this)); this.chatNs.on('disconnect', this.onDisconnect.bind(this)); } public async addListener(eventName: string, listener: (any) => void) { if (this.isConnected) { this.chatNs.on(eventName, listener); } } public async removeListener(eventName: string, listener?: (any) => void) { if (this.initialized()) this.chatNs.off(eventName, listener); } public sendMessage(message: Record<string, any>, channel: IChannel) { if (this.isConnected) { this.chatNs.emit(WebsocketMessageNames.ChatMessage, { socket_room: ChatNamespace.channelToRoomId(channel), ...message }) } } public connectToChannels(channel_ids: string[]) { if (this.isConnected) this.chatNs.emit(WebsocketMessageNames.JoinChatChannel, channel_ids) } public disconnectFromChannels(channel_ids: string[]) { if (this.isConnected) this.chatNs.emit(WebsocketMessageNames.LeaveChatChannel, channel_ids) } private onConnect() { this._isConnected = true; console.log('Chat namespace connected!') if (!_.isEmpty(this.channels) && !this.initialized()) { this.initialize(); } } private onDisconnect(reason) { this._isConnected = false; console.log(reason) } public get isConnected() { return this._isConnected; } public hasChannels() { return !_.isEmpty(this.channels) } public initialized() { return this._initialized } public async refreshChannels(channels: any) { this.channels = {} channels.forEach(c => { this.channels[c.id] = { unread: 0, ...c } }); if (this.isConnected && !this.initialized()) { await this.initialize(); } } public async initialize() { console.log("Initializing chat state") this.addListener(WebsocketMessageNames.ChatMessage, this.onMessage.bind(this)) this.connectToChannels(Object.values(this.channels).map(ChatNamespace.channelToRoomId)) this._initialized = true; } public async deinit() { this._initialized = false; this.removeListener(WebsocketMessageNames.ChatMessage, this.onMessage.bind(this)) this.disconnectFromChannels(Object.values(this.channels).map(ChatNamespace.channelToRoomId)) this.channels = {} } public async reinit() { console.log("re-initializing chat state") const raw_channels = await this.getChatMessages() const channels = {} raw_channels.forEach(c => { channels[c.id] = { unread: this.channels[c.id] || 0, ...c } }); // channels is the new channels, this.channels is the old channels // new_channel_ids are the ids in channels which are not in this.channels const new_channel_ids = Object.keys(channels).filter(x => !Object.keys(this.channels).includes(x)); // removed_channel_ids are the ids in this.channels which are not in channels const removed_channel_ids = Object.keys(this.channels).filter(x => !Object.keys(channels).includes(x)); this.disconnectFromChannels(removed_channel_ids.map(id => this.channels[id]).map(ChatNamespace.channelToRoomId)) this.connectToChannels(new_channel_ids.map(id => channels[id]).map(ChatNamespace.channelToRoomId)) this.channels = channels } private onMessage(msg) { console.log("onMessage") // Ignore message if it is already in ChatMessages, last 5 msgs if(this.channels[msg.chat_channel_id].ChatMessages.slice(-5).includes(msg)) { return } this.channels[msg.chat_channel_id].ChatMessages.push(msg) this.channels[msg.chat_channel_id].unread++ if (this.activeChannel == msg.chat_channel_id) { this.channels[msg.chat_channel_id].unread = 0; } m.redraw(); } public readMessages(channel_id: string) { this.channels[channel_id].unread = 0; } private static channelToRoomId(channel: IChannel) { return `${channel.chain_id}-${channel.id}` } public async createChatChannel(name, chain_id, category) { // this call will fail unless its an admin try { const res = await $.post(`${app.serverUrl()}/createChatChannel`, { jwt: app.user.jwt, name, chain_id, category }) if(res.status !== "200"){ throw new Error("Failed to create chat channel") } await this.reinit() return true } catch (e) { console.error(e) } } public async getChatMessages() { if(!app.user.activeAccount) { throw new Error(ChatErrors.NOT_LOGGED_IN) } try { const res = await $.get(`${app.serverUrl()}/getChatMessages`, { jwt: app.user.jwt, address: app.user.activeAccount.address, chain_id: app.activeChainId() }) if(res.status !== "200") { throw new Error('Failed to get chat messages') } const raw = JSON.parse(res.result) return raw } catch (e) { console.error(e) return [] } } public async deleteChatChannel(channel_id: number) { try { const response = await $.ajax({ url: `${app.serverUrl()}/deleteChatChannel`, data: { channel_id, chain_id: app.activeChainId(), jwt: app.user.jwt }, type: 'DELETE' }); if (response.status !== 'Success') { throw new Error("Failed to delete chat channel") } await this.reinit() return true } catch (e) { console.error(e) return false } } public async deleteChatCategory(category: string) { try { const response = await $.ajax({ url: `${app.serverUrl()}/deleteChatCategory`, data: { category, chain_id: app.activeChainId(), jwt: app.user.jwt, }, type: 'DELETE' }); if (response.status !== 'Success') { throw new Error("Failed to delete chat category") } await this.reinit() return true } catch (e) { console.error(e) return false } } public async renameChatCategory(category: string, new_category: string) { try { const response = await $.ajax({ url: `${app.serverUrl()}/renameChatCategory`, data: { category, new_category, chain_id: app.activeChainId(), jwt: app.user.jwt }, type: 'PUT' }); if (response.status !== 'Success') { throw new Error("Failed to rename chat category") } await this.reinit() return true } catch (e) { console.error(e) return false } } public async renameChatChannel(channel_id: number, name: string) { try { const response = await $.ajax({ url: `${app.serverUrl()}/renameChatChannel`, data: { channel_id, name, chain_id: app.activeChainId(), jwt: app.user.jwt }, type: 'PUT' }); if (response.status !== 'Success') { throw new Error("Failed to rename chat channel") } await this.reinit() return true } catch (e) { console.error(e) return false } } }