551 lines
12 KiB
JavaScript
551 lines
12 KiB
JavaScript
export default class ContentController{
|
|
|
|
/* (1) Construct default attributes
|
|
*
|
|
---------------------------------------------------------*/
|
|
constructor(){
|
|
|
|
/* (1) Websocket re-connection policy */
|
|
this.attempt = {
|
|
max: 5,
|
|
count: 1,
|
|
_default_timeout: 500, // in ms
|
|
get timeout(){
|
|
|
|
// return timeout + increment it for next time
|
|
return (this._default_timeout/2) * Math.pow(2, this.count++);
|
|
},
|
|
// same without incrementing (for LOGS)
|
|
get raw_timeout(){ return (this._default_timeout/2) * Math.pow(2, this.count); }
|
|
};
|
|
|
|
/* (2) Manage updates in connection status (ONLINE - OFFLINE) */
|
|
window.addEventListener('online', function(){
|
|
DEBUG_MOD && console.log(`[NETWORK] online`);
|
|
|
|
DEBUG_MOD && console.log(` + Try to connect`);
|
|
// wait 500ms for net to be really online
|
|
setTimeout( this.ws_connect.bind(this), 500);
|
|
|
|
}.bind(this));
|
|
|
|
window.addEventListener('offline', function(){
|
|
|
|
DEBUG_MOD && console.log(`[NETWORK] offline`);
|
|
gs.get.connection = 0; // connection status bar
|
|
|
|
DEBUG_MOD && console.log(` + stop resync-strategy`);
|
|
csock.onclose = function(){}; // stop propagating recursive ws_connect()
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
/* (2) Channel bindings
|
|
*
|
|
---------------------------------------------------------*/
|
|
get cid(){ return gs.get.channel.current; }
|
|
|
|
get cbuf(){ return gs.get.channel._buffer; }
|
|
|
|
|
|
/* (3) Room ID binding
|
|
*
|
|
---------------------------------------------------------*/
|
|
get rid(){ return gs.get.room.text.current; }
|
|
get rbuf(){ return gs.get.room._buffer.text; }
|
|
|
|
get messages(){ return this.rbuf.messages; }
|
|
get members(){ return this.rbuf.members; }
|
|
|
|
// current user data
|
|
get uid(){ return gs.get.auth.user.uid; }
|
|
get ubuf(){ return gs.get.auth.user; }
|
|
|
|
|
|
/* (5) User getter
|
|
*
|
|
* @user_id<int> User id
|
|
*
|
|
* @return user<array> User data
|
|
*
|
|
---------------------------------------------------------*/
|
|
user(user_id=null){
|
|
|
|
/* (1) Error: if invalid user_id */
|
|
if( isNaN(user_id) )
|
|
return {};
|
|
|
|
/* (2) Error: unknown user */
|
|
if( this.cbuf.users == null || this.cbuf.users.length < 1 )
|
|
return {};
|
|
|
|
/* (3) return user data */
|
|
for( let u of this.cbuf.users )
|
|
if( u.uid === user_id )
|
|
return u;
|
|
|
|
/* (4) Error */
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
/* (6) Change username
|
|
*
|
|
* @username<String> New username
|
|
---------------------------------------------------------*/
|
|
change_username(username=null){
|
|
|
|
/* (1) Error: if invalid user_id */
|
|
if( typeof username !== 'string' )
|
|
return false;
|
|
|
|
/* (2) Error: unknown user */
|
|
if( this.uid == null )
|
|
return false;
|
|
|
|
/* (3) Call api UPDATE */
|
|
api.call(`PUT /user/${this.uid}`, { username: username }, function(rs){
|
|
|
|
gs.get.popup.hide();
|
|
|
|
// manage error
|
|
if( rs.error !== 0 )
|
|
return;
|
|
|
|
// update global username
|
|
let tmp_user = auth.user;
|
|
tmp_user.username = username;
|
|
auth.user = tmp_user;
|
|
|
|
}.bind(this), auth.token);
|
|
|
|
/* (4) Error */
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
/* (7) Change password
|
|
*
|
|
* @password<String> New password
|
|
*
|
|
---------------------------------------------------------*/
|
|
change_password(password=null){
|
|
|
|
/* (1) Error: if invalid user_id */
|
|
if( typeof password !== 'string' )
|
|
return false;
|
|
|
|
/* (2) Error: unknown user */
|
|
if( this.uid == null )
|
|
return false;
|
|
|
|
/* (3) Call api UPDATE */
|
|
api.call(`PUT /user/${this.uid}`, { password: password }, () => gs.get.popup.hide(), auth.token);
|
|
|
|
/* (4) Error */
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
/* (8) Send message
|
|
*
|
|
---------------------------------------------------------*/
|
|
send_message(_msg=null){
|
|
|
|
/* (1) Manage invalid _msg */
|
|
if( typeof _msg !== 'string' || _msg.length <= 0 )
|
|
return true;
|
|
|
|
/* (2) Send message */
|
|
window.csock.send({ buffer: {
|
|
rid: this.rid,
|
|
mid: null,
|
|
message: _msg
|
|
}});
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
/* (9) Websocket connection / reconnection
|
|
*
|
|
---------------------------------------------------------*/
|
|
ws_connect(){
|
|
|
|
gs.get.connection = 1;
|
|
|
|
// 1. Close websocket if exists
|
|
if( window.csock instanceof wscd ){
|
|
csock.onclose = function(){}; // stop propagating recursive ws_connect()
|
|
csock.close();
|
|
}
|
|
|
|
// 2. Create new connection
|
|
window.csock = new wscd(`wss://ws.douscord.xdrm.io/channel/${this.cid}`, { token: auth.token });
|
|
|
|
// 3. Bind events
|
|
csock.onconnected = () => {
|
|
|
|
DEBUG_MOD && console.log('[WS] connected');
|
|
DEBUG_MOD && console.log('[WS] pop stack', csock.stack.map( v => v.buffer ) );
|
|
|
|
// show connection status
|
|
gs.get.connection = 2;
|
|
|
|
// reset attempt count
|
|
gs.get.content.attempt.count = 1;
|
|
|
|
setTimeout( () => { gs.get.connection = null; }, 1500);
|
|
};
|
|
csock.onreceive = gs.get.content.ws_handler.bind({ event: 'receive', class: this });
|
|
csock.onclose = gs.get.content.ws_handler.bind({ event: 'close', class: this });
|
|
|
|
// 4. Start communication
|
|
csock.bind();
|
|
|
|
// 5. Send RID (useful when reconnecting, but a doublon at first connection)
|
|
csock.send({ buffer: { rid: gs.get.room.text.current } });
|
|
|
|
DEBUG_MOD && console.log('[WS] connecting');
|
|
DEBUG_MOD && console.log(` + start resync-strategy`);
|
|
DEBUG_MOD && console.log(`[WS] push stack`, { rid: gs.get.room.text.current });
|
|
|
|
}
|
|
|
|
|
|
/* (10) Websocket connection manager
|
|
*
|
|
* @this.event<String> Event type :
|
|
* 'close' -> socket closed
|
|
* 'receive' -> received message
|
|
*
|
|
---------------------------------------------------------*/
|
|
ws_handler(_response){
|
|
|
|
DEBUG_MOD && console.groupCollapsed(`[WS] on${this.event}`);
|
|
|
|
/* (1) Manage error */
|
|
if( this.event === null )
|
|
return;
|
|
|
|
|
|
/* (2) CLOSE event -> reconnect
|
|
---------------------------------------------------------*/
|
|
if( this.event === 'close' ){
|
|
|
|
// 1. update connection status bar
|
|
gs.get.connection = 0;
|
|
|
|
// 2. Do nothing if offline (online trigger will do the job when online again)
|
|
if( !navigator.onLine ){
|
|
DEBUG_MOD && console.log(' + network offline, wait for online trigger');
|
|
return DEBUG_MOD && console.groupEnd();
|
|
}
|
|
|
|
|
|
// 3. if max attempt exceeded -> logout user
|
|
if( this.class.attempt.count > this.class.attempt.max ){
|
|
DEBUG_MOD && console.log(' + max resync offset reached');
|
|
DEBUG_MOD && console.log(' + logout');
|
|
auth.token = null;
|
|
gs.get.refresh();
|
|
return DEBUG_MOD && console.groupEnd();
|
|
}
|
|
|
|
// 4. Try to reconnect
|
|
DEBUG_MOD && console.log(` + resync (${this.class.attempt.count}/${this.class.attempt.max}) in ${this.class.attempt.raw_timeout}ms`);
|
|
setTimeout(this.class.ws_connect.bind(this.class), this.class.attempt.timeout);
|
|
return DEBUG_MOD && console.groupEnd();
|
|
}
|
|
|
|
|
|
/* (3) RECEIVE event
|
|
---------------------------------------------------------*/
|
|
if( this.event === 'receive' ){
|
|
|
|
/* (1) Communication error -> reconnect in 500ms */
|
|
if( typeof _response !== 'object' ){
|
|
DEBUG_MOD && console.log(` + invalid response: resync in 500ms`);
|
|
setTimeout(this.class.ws_connect.bind(this.class), 500);
|
|
return DEBUG_MOD && console.groupEnd();
|
|
}
|
|
|
|
/* (2) If message update -> update interface model */
|
|
this.class.ws_to_model(_response);
|
|
return DEBUG_MOD && console.groupEnd();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* (11) MAIN UPDATER
|
|
*
|
|
---------------------------------------------------------*/
|
|
ws_to_model(_dat){
|
|
|
|
|
|
DEBUG_MOD && console.group(`room`);
|
|
|
|
/* (1) Manage rooms DELETE
|
|
---------------------------------------------------------*/
|
|
/* (1) Extract ids */
|
|
let room_ids = Object.keys(_dat.room).map( (v) => parseInt(v) );
|
|
let current_list = gs.get.room;
|
|
|
|
/* (2) Manage DELETED rooms */
|
|
for( let t in current_list ){
|
|
|
|
for( let ri in current_list[t].list ){
|
|
|
|
// if existing room is not in received keys -> has been deleted
|
|
let to_remove = room_ids.indexOf(current_list[t].list[ri].id) < 0;
|
|
|
|
// delete room from interface
|
|
DEBUG_MOD && to_remove && console.log(` + #${current_list[t].list[ri].id} dropped`);
|
|
( to_remove ) && current_list[t].list.splice(ri,1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/* (2) Manage rooms CREATE + UPDATE
|
|
---------------------------------------------------------*/
|
|
for( let ri of room_ids ){
|
|
|
|
// 1. Extract room data
|
|
let room = _dat.room[ri];
|
|
|
|
// 2. if room data is null -> ignore
|
|
if( room === null )
|
|
continue;
|
|
|
|
// 3. Manage room 'type'
|
|
room.type = (room.type === 0) ? 'text' : 'voice';
|
|
|
|
// 4. Check whether room already exists in interface
|
|
let existing_index = -1;
|
|
|
|
|
|
main_loop: for( let t in current_list ){
|
|
|
|
for( let r in current_list[t].list ){
|
|
|
|
if( current_list[t].list[r].id === ri ){
|
|
existing_index = r;
|
|
break main_loop;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// 5. Create room
|
|
if( existing_index < 0 ){
|
|
|
|
DEBUG_MOD && console.log(` + #${ri} [${room.type}] room created`);
|
|
gs.get.room.dump([{
|
|
rid: ri,
|
|
name: room.name,
|
|
messages: room.messages,
|
|
members: room.members,
|
|
type: room.type
|
|
}], true);
|
|
|
|
continue;
|
|
}
|
|
|
|
// 5. Update room
|
|
current_list[room.type].list[existing_index].name = room.name;
|
|
current_list[room.type].list[existing_index].members = room.members;
|
|
|
|
// 6. We are done if VOICE room
|
|
if( room.type === 'voice' )
|
|
continue;
|
|
|
|
|
|
// 7. Push new messages
|
|
DEBUG_MOD && ( room.messages.length > 0 ) && console.log(` + #${ri} has ${room.messages.length} new message(s)`);
|
|
|
|
for( let m of room.messages ){
|
|
|
|
current_list[room.type].list[existing_index].messages.push({
|
|
uid: m.uid,
|
|
mid: m.mid,
|
|
msg: m.content,
|
|
ts: m.ts
|
|
});
|
|
|
|
}
|
|
|
|
// 8. Notification API -> if not current channel
|
|
if( room.messages.length > 0 && ri !== gs.get.content.rid ){
|
|
|
|
let title = `Room #${room.name}`;
|
|
let body = `${room.messages.length} new messages`;
|
|
new Notification(title, { body: body });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
DEBUG_MOD && console.groupEnd();
|
|
|
|
|
|
|
|
|
|
DEBUG_MOD && console.group(`channel`);
|
|
DEBUG_MOD && ( _dat.channels.add.length > 0 ) && console.log(` + ${_dat.channels.add.length} new`);
|
|
DEBUG_MOD && ( _dat.channels.rem.length > 0 ) && console.log(` + ${_dat.channels.rem.length} dropped`);
|
|
DEBUG_MOD && ( _dat.channels.upd.length > 0 ) && console.log(` + ${_dat.channels.upd.length} updated`);
|
|
|
|
/* (3) Manage channels DELETE
|
|
---------------------------------------------------------*/
|
|
for( let c of _dat.channels.rem ){
|
|
|
|
for( let ci in gs.get.channel.list ){
|
|
|
|
// 1. Local copy channel data
|
|
let channel = gs.get.channel.list[ci];
|
|
|
|
// 2. If id matches -> REMOVE
|
|
if( channel.id === c.id ){
|
|
|
|
// 2.1. If remove CURRENT channel -> nav to channel 1
|
|
if( channel.id === gs.get.content.cid )
|
|
gs.get.channel.nav(1);
|
|
|
|
// 2.2. Delete channel
|
|
DEBUG_MOD && console.log(` + #${channel.id} dropped`);
|
|
gs.get.channel.list.splice(ci, 1);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* (4) Manage channels CREATE
|
|
---------------------------------------------------------*/
|
|
for( let c of _dat.channels.add ){
|
|
|
|
DEBUG_MOD && console.log(` + #${c.id} [/${c.link}] created`);
|
|
|
|
gs.get.channel.dump([{
|
|
id: parseInt(c.id),
|
|
label: c.name,
|
|
link: c.link
|
|
}], true);
|
|
|
|
}
|
|
|
|
|
|
/* (5) Manage channels UPDATE
|
|
---------------------------------------------------------*/
|
|
for( let c of _dat.channels.upd ){
|
|
|
|
for( let ci in gs.get.channel.list ){
|
|
|
|
// 1. Local copy channel data
|
|
let channel = gs.get.channel.list[ci];
|
|
|
|
// 2. If id matches -> UPDATE
|
|
if( channel.id === c.id ){
|
|
|
|
DEBUG_MOD && console.log(` + #${c.id} updated`);
|
|
|
|
gs.get.channel.list[ci].label = c.name;
|
|
gs.get.channel.list[ci].link = c.link;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
DEBUG_MOD && console.groupEnd();
|
|
DEBUG_MOD && console.group('users');
|
|
|
|
|
|
|
|
|
|
let userset = gs.get.content.cbuf.users;
|
|
|
|
|
|
/* (6) Manage users DELETE
|
|
---------------------------------------------------------*/
|
|
for( let u of _dat.users.rem ){
|
|
|
|
for( let ui in userset ){
|
|
|
|
// 1. Local copy user data
|
|
let user = userset[ui];
|
|
|
|
// 2. If id matches -> REMOVE
|
|
if( user.uid === u.id ){
|
|
DEBUG_MOD && console.log(` + '${user.username}' dropped`);
|
|
userset.splice(ui, 1);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* (7) Manage users CREATE
|
|
---------------------------------------------------------*/
|
|
for( let u of _dat.users.add ){
|
|
|
|
DEBUG_MOD && console.log(` + '${u.name}' created`);
|
|
userset.push({
|
|
uid: parseInt(u.id),
|
|
username: u.name
|
|
});
|
|
|
|
}
|
|
|
|
|
|
/* (8) Manage users UPDATE
|
|
---------------------------------------------------------*/
|
|
for( let u of _dat.users.upd ){
|
|
|
|
for( let ui in userset ){
|
|
|
|
// 1. Local copy user data
|
|
let user = userset[ui];
|
|
|
|
// 2. If id matches -> UPDATE
|
|
if( user.uid === u.id ){
|
|
DEBUG_MOD && console.log(` + '${user.username}' renamed '${u.name}'`);
|
|
userset[ui].username = u.name;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
DEBUG_MOD && console.groupEnd();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |