const HASH_EXTRACTOR = /^\#\/topic\/(?[A-Za-z\-]+)\/?(?\w+)?/gm; const UPDATE_TOPICS_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: { topic_id, view }, } = HASH_EXTRACTOR.exec(window.location.hash ?? "") ?? { groups: {}, }; /* console.dir({ url: window.location.href, hash: window.location.hash, topic_id, view, }); */ if (!document.body.dataset.topic || document.body.dataset.topic !== topic_id) { const previous = document.body.dataset.topic; /* console.dir({ topic_changed: { detail: { previous, topic_id, }, }, }); */ document.body.dataset.topic = topic_id; this._emit( 'topic_changed', { previous, topic_id }); if (!topic_id) { const first_topic_id = this.TOPICS.TOPIC_LIST[0]?.id; if (first_topic_id) { window.location.hash = `/topic/${first_topic_id}/chat`; // TODO: allow a different default than chat } } } if (!document.body.dataset.view || document.body.dataset.view !== view) { const previous = document.body.dataset.view; document.body.dataset.view = 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.TOPICS.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.TOPICS.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]; }, }, TOPICS: { _last_topic_update: undefined, _update_topics_timeout: undefined, TOPIC_LIST: [], update: async () => { const now = new Date(); const time_since_last_update = now - (APP.TOPICS._last_topic_update ?? 0); if (time_since_last_update < UPDATE_TOPICS_FREQUENCY / 2) { return; } if (APP.TOPICS._update_topics_timeout) { clearTimeout(APP.TOPICS._update_topics_timeout); APP.TOPICS._update_topics_timeout = undefined; } try { const topics_response = await api.fetch("/api/topics"); if (topics_response.ok) { const new_topics = await topics_response.json(); const has_differences = APP.TOPICS.TOPIC_LIST.length !== new_topics.length || new_topics.some((topic, index) => { return ( APP.TOPICS.TOPIC_LIST[index]?.id !== topic.id || APP.TOPICS.TOPIC_LIST[index]?.name !== topic.name ); }); if (has_differences) { APP.TOPICS.TOPIC_LIST = [...new_topics]; APP._emit( 'topics_updated', { topics: APP.TOPICS.TOPIC_LIST }); } APP.TOPICS._last_topic_update = now; } } catch (error) { console.error(error); } APP.TOPICS._update_topics_timeout = setTimeout( APP.TOPICS.update, UPDATE_TOPICS_FREQUENCY, ); // now that we have topics, make sure our url is all good APP.extract_url_hash_info(); }, }, }; document.addEventListener("DOMContentLoaded", APP.load.bind( APP ));