2025-11-08 17:15:26 -08:00
|
|
|
const HASH_EXTRACTOR = /^\#\/(?<view>\w+)(?:\/channel\/(?<channel_id>[A-Za-z\-]+)\/?)?/gm;
|
|
|
|
|
const UPDATE_CHANNELS_FREQUENCY = 60_000;
|
2025-10-25 14:57:28 -07:00
|
|
|
|
|
|
|
|
const APP = {
|
2025-10-25 19:44:07 -07:00
|
|
|
user: undefined,
|
2025-10-25 14:57:28 -07:00
|
|
|
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 {
|
2025-11-08 17:15:26 -08:00
|
|
|
groups: { view, channel_id },
|
2025-10-25 14:57:28 -07:00
|
|
|
} = HASH_EXTRACTOR.exec(window.location.hash ?? "") ?? {
|
|
|
|
|
groups: {},
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-09 13:16:49 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._emit( 'view_changed', {
|
|
|
|
|
previous,
|
|
|
|
|
view
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-10-25 14:57:28 -07:00
|
|
|
|
2025-11-08 17:15:26 -08:00
|
|
|
if (!document.body.dataset.channel || document.body.dataset.channel !== channel_id) {
|
2025-11-09 13:01:56 -08:00
|
|
|
const previous = typeof document.body.dataset.channel === 'string' ? document.body.dataset.channel : undefined;
|
2025-10-25 14:57:28 -07:00
|
|
|
|
2025-11-09 13:01:56 -08:00
|
|
|
if ( channel_id ) {
|
|
|
|
|
document.body.dataset.channel = channel_id;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
delete document.body.dataset.channel;
|
|
|
|
|
}
|
2025-10-25 14:57:28 -07:00
|
|
|
|
2025-11-09 13:01:56 -08:00
|
|
|
const target_channel_id = channel_id ?? this.CHANNELS.CHANNEL_LIST[0]?.id;
|
|
|
|
|
|
|
|
|
|
// TODO: allow a different default than chat
|
2025-11-09 13:16:49 -08:00
|
|
|
const hash_target = `/${ view ? view : 'chat' }` + ( target_channel_id ? `/channel/${ target_channel_id }` : '' );
|
2025-10-25 14:57:28 -07:00
|
|
|
|
2025-11-09 13:01:56 -08:00
|
|
|
if ( window.location.hash?.slice( 1 ) !== hash_target ) {
|
|
|
|
|
if ( previous !== target_channel_id ) {
|
|
|
|
|
this._emit( 'channel_changed', {
|
|
|
|
|
previous,
|
|
|
|
|
channel_id: target_channel_id
|
|
|
|
|
});
|
2025-10-25 14:57:28 -07:00
|
|
|
}
|
2025-11-09 13:01:56 -08:00
|
|
|
|
|
|
|
|
window.location.hash = hash_target;
|
2025-10-25 14:57:28 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
load: async function() {
|
2025-10-25 15:12:55 -07:00
|
|
|
this.server = {
|
|
|
|
|
name: document.title,
|
|
|
|
|
url: window.location.origin ?? window.location.href,
|
|
|
|
|
icon: '/icons/favicon-128x128.png',
|
|
|
|
|
icon_background: undefined
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-25 14:57:28 -07:00
|
|
|
this.suggested_servers = [];
|
|
|
|
|
try {
|
|
|
|
|
const server_info_response = await api.fetch( '/files/settings/settings.json' );
|
2025-10-25 15:12:55 -07:00
|
|
|
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
|
|
|
|
|
};
|
2025-10-25 14:57:28 -07:00
|
|
|
}
|
|
|
|
|
|
2025-10-25 15:12:55 -07:00
|
|
|
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();
|
|
|
|
|
}
|
2025-10-25 14:57:28 -07:00
|
|
|
}
|
|
|
|
|
catch( error ) {
|
|
|
|
|
console.error( error );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.addEventListener("locationchange", this.extract_url_hash_info.bind( this ));
|
2025-11-08 17:15:26 -08:00
|
|
|
window.addEventListener("locationchange", this.CHANNELS.update );
|
2025-10-25 14:57:28 -07:00
|
|
|
|
|
|
|
|
this.check_if_logged_in();
|
|
|
|
|
this.extract_url_hash_info();
|
|
|
|
|
this._emit( 'load', this );
|
|
|
|
|
},
|
|
|
|
|
|
2025-10-25 19:44:07 -07:00
|
|
|
update_user: async function( user ) {
|
|
|
|
|
this.user = user;
|
|
|
|
|
|
2025-10-25 14:57:28 -07:00
|
|
|
document.body.dataset.user = JSON.stringify(user);
|
|
|
|
|
document.body.dataset.perms = user.permissions.join(":");
|
|
|
|
|
|
2025-11-08 17:15:26 -08:00
|
|
|
this.CHANNELS.update();
|
2025-10-25 14:57:28 -07:00
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-08 17:15:26 -08:00
|
|
|
CHANNELS: {
|
|
|
|
|
_last_channel_update: undefined,
|
|
|
|
|
_update_channels_timeout: undefined,
|
|
|
|
|
CHANNEL_LIST: [],
|
2025-10-25 14:57:28 -07:00
|
|
|
|
2025-11-09 13:01:56 -08:00
|
|
|
update: async ( force = false ) => {
|
2025-10-25 14:57:28 -07:00
|
|
|
const now = new Date();
|
2025-11-08 17:15:26 -08:00
|
|
|
const time_since_last_update = now - (APP.CHANNELS._last_channel_update ?? 0);
|
2025-11-09 13:01:56 -08:00
|
|
|
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 ) {
|
2025-10-25 14:57:28 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 17:15:26 -08:00
|
|
|
if (APP.CHANNELS._update_channels_timeout) {
|
|
|
|
|
clearTimeout(APP.CHANNELS._update_channels_timeout);
|
|
|
|
|
APP.CHANNELS._update_channels_timeout = undefined;
|
2025-10-25 14:57:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-08 17:15:26 -08:00
|
|
|
const channels_response = await api.fetch("/api/channels");
|
|
|
|
|
if (channels_response.ok) {
|
|
|
|
|
const new_channels = await channels_response.json();
|
2025-10-25 14:57:28 -07:00
|
|
|
const has_differences =
|
2025-11-08 17:15:26 -08:00
|
|
|
APP.CHANNELS.CHANNEL_LIST.length !== new_channels.length ||
|
|
|
|
|
new_channels.some((channel, index) => {
|
2025-10-25 14:57:28 -07:00
|
|
|
return (
|
2025-11-08 17:15:26 -08:00
|
|
|
APP.CHANNELS.CHANNEL_LIST[index]?.id !== channel.id ||
|
|
|
|
|
APP.CHANNELS.CHANNEL_LIST[index]?.name !== channel.name
|
2025-10-25 14:57:28 -07:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (has_differences) {
|
2025-11-08 17:15:26 -08:00
|
|
|
APP.CHANNELS.CHANNEL_LIST = [...new_channels];
|
2025-10-25 14:57:28 -07:00
|
|
|
|
2025-11-08 17:15:26 -08:00
|
|
|
APP._emit( 'channels_updated', {
|
|
|
|
|
channels: APP.CHANNELS.CHANNEL_LIST
|
2025-10-25 14:57:28 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 17:15:26 -08:00
|
|
|
APP.CHANNELS._last_channel_update = now;
|
2025-10-25 14:57:28 -07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 17:15:26 -08:00
|
|
|
APP.CHANNELS._update_channels_timeout = setTimeout(
|
|
|
|
|
APP.CHANNELS.update,
|
|
|
|
|
UPDATE_CHANNELS_FREQUENCY,
|
2025-10-25 14:57:28 -07:00
|
|
|
);
|
|
|
|
|
|
2025-11-08 17:15:26 -08:00
|
|
|
// now that we have channels, make sure our url is all good
|
2025-10-25 14:57:28 -07:00
|
|
|
APP.extract_url_hash_info();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", APP.load.bind( APP ));
|