const HASH_EXTRACTOR = /^\#\/(?\w+)(?:\/channel\/(?[A-Za-z\-]+)\/?)?/gm; const UPDATE_CHANNELS_FREQUENCY = 60_000; const APP = { user: undefined, user_servers: [], user_watches: [], _event_callbacks: {}, on: function( event_name, callback ) { this._event_callbacks[ event_name ] = this._event_callbacks[ event_name ] ?? new Set(); this._event_callbacks[event_name ].add( callback ); return true; }, off: function( event_name, callback ) { return this._event_callbacks[ event_name ]?.delete( callback ); }, _emit: function( event_name, event_data ) { const event_callbacks = this._event_callbacks[ event_name ]; event_callbacks?.forEach( ( callback ) => { callback( event_data ); }); }, check_if_logged_in: async function () { try { const session_response = await api.fetch("/api/users/me"); if (!session_response.ok) { const error_body = await session_response.json(); const error = error_body?.error; console.dir({ error_body, error, }); return; } const user = await session_response.json(); this.login( user ); } catch (error) { console.dir({ error, }); } }, extract_url_hash_info: async function () { HASH_EXTRACTOR.lastIndex = 0; // ugh, need this to have this work on multiple exec calls const { groups: { view, channel_id }, } = HASH_EXTRACTOR.exec(window.location.hash ?? "") ?? { groups: {}, }; /* console.dir({ url: window.location.href, hash: window.location.hash, view, channel_id, }); */ if (!document.body.dataset.channel || document.body.dataset.channel !== channel_id) { const previous = typeof document.body.dataset.channel === 'string' ? document.body.dataset.channel : undefined; /* console.dir({ channel_changed: { detail: { previous, channel_id, }, }, }); */ if ( channel_id ) { document.body.dataset.channel = channel_id; } else { delete document.body.dataset.channel; } const target_channel_id = channel_id ?? this.CHANNELS.CHANNEL_LIST[0]?.id; // TODO: allow a different default than chat const hash_target = '/chat' + ( target_channel_id ? `/channel/${ target_channel_id }` : '' ); if ( window.location.hash?.slice( 1 ) !== hash_target ) { if ( previous !== target_channel_id ) { this._emit( 'channel_changed', { previous, channel_id: target_channel_id }); } window.location.hash = hash_target; } } if (!document.body.dataset.view || document.body.dataset.view !== view) { const previous = typeof document.body.dataset.view === 'string' ? document.body.dataset.view : undefined; if ( view ) { document.body.dataset.view = view; } else { delete document.body.dataset.view; } /* console.dir({ view_changed: { detail: { previous, view, }, }, }); */ this._emit( 'view_changed', { previous, view }); } }, load: async function() { this.server = { name: document.title, url: window.location.origin ?? window.location.href, icon: '/icons/favicon-128x128.png', icon_background: undefined }; this.suggested_servers = []; try { const server_info_response = await api.fetch( '/files/settings/settings.json' ); if ( server_info_response.ok ) { const this_server_info = await server_info_response.json(); this.server = { name: this_server_info.name ?? this.server.name, url: this_server_info.url ?? this.server.url, icon: this_server_info.icon ?? this.server.icon, icon_background: this_server_info.icon_background ?? this.server.icon_background }; } const suggested_servers_response = await api.fetch( '/files/settings/suggested_servers.json' ); if ( suggested_servers_response.ok ) { this.suggested_servers = await suggested_servers_response.json(); } } catch( error ) { console.error( error ); } window.addEventListener("locationchange", this.extract_url_hash_info.bind( this )); window.addEventListener("locationchange", this.CHANNELS.update ); this.check_if_logged_in(); this.extract_url_hash_info(); this._emit( 'load', this ); }, update_user: async function( user ) { this.user = user; document.body.dataset.user = JSON.stringify(user); document.body.dataset.perms = user.permissions.join(":"); this.CHANNELS.update(); this.user_servers = []; try { const user_server_response = await api.fetch( `/files/users/${ user.id }/settings/servers.json` ); this.user_servers = user_server_response.ok ? await user_server_response.json() : []; } catch( error ) { console.error( error ); } this.user_watches = []; try { const user_watches_response = await api.fetch( `/api/users/${ user.id }/watches` ); this.user_watches = user_watches_response.ok ? await user_watches_response.json() : []; } catch( error ) { console.error( error ); } // TODO: show unread indicators based on watches }, login: async function( user ) { await this.update_user( user ); this._emit( 'user_logged_in', { user } ); }, logout: function() { delete document.body.dataset.user; delete document.body.dataset.perms; window.location = "/"; this._emit( "user_logged_out", {}); }, USERS: { _evict_timeouts: {}, _update_timeouts: {}, get: async (id, force) => { if (force || !APP.USERS[id]) { APP.USERS[id] = await (await api.fetch(`/api/users/${id}`)).json(); } if (!APP.USERS._update_timeouts[id]) { APP.USERS._update_timeouts[id] = setInterval(() => { APP.USERS.get(id, true); }, 1 * 60_000); } if (!force) { if (APP.USERS._evict_timeouts[id]) { clearTimeout(APP.USERS._evict_timeouts[id]); } APP.USERS._evict_timeouts[id] = setTimeout(() => { if (APP.USERS._update_timeouts[id]) { clearTimeout(APP.USERS._update_timeouts[id]); delete APP.USERS._update_timeouts[id]; } delete APP.USERS[id]; }, 10 * 60_000); } return APP.USERS[id]; }, }, CHANNELS: { _last_channel_update: undefined, _update_channels_timeout: undefined, CHANNEL_LIST: [], update: async ( force = false ) => { const now = new Date(); const time_since_last_update = now - (APP.CHANNELS._last_channel_update ?? 0); const sufficient_time_has_passed_since_last_update = time_since_last_update > UPDATE_CHANNELS_FREQUENCY / 2; if ( !force && !sufficient_time_has_passed_since_last_update ) { return; } if (APP.CHANNELS._update_channels_timeout) { clearTimeout(APP.CHANNELS._update_channels_timeout); APP.CHANNELS._update_channels_timeout = undefined; } try { const channels_response = await api.fetch("/api/channels"); if (channels_response.ok) { const new_channels = await channels_response.json(); const has_differences = APP.CHANNELS.CHANNEL_LIST.length !== new_channels.length || new_channels.some((channel, index) => { return ( APP.CHANNELS.CHANNEL_LIST[index]?.id !== channel.id || APP.CHANNELS.CHANNEL_LIST[index]?.name !== channel.name ); }); if (has_differences) { APP.CHANNELS.CHANNEL_LIST = [...new_channels]; APP._emit( 'channels_updated', { channels: APP.CHANNELS.CHANNEL_LIST }); } APP.CHANNELS._last_channel_update = now; } } catch (error) { console.error(error); } APP.CHANNELS._update_channels_timeout = setTimeout( APP.CHANNELS.update, UPDATE_CHANNELS_FREQUENCY, ); // now that we have channels, make sure our url is all good APP.extract_url_hash_info(); }, }, }; document.addEventListener("DOMContentLoaded", APP.load.bind( APP ));