let m = require('mithril'); let rs = require('rswebui'); let people_util = require('people/people_util'); // **************** utility functions ******************** function loadLobbyDetails(id, apply) { rs.rsJsonApiRequest('/rsMsgs/getChatLobbyInfo', { id, }, detail => { if (detail.retval) { apply(detail.info); } }, true, {}, undefined, // Custom serializer NOTE: // Since id represents 64-bit int(see deserializer note below) // Instead of using JSON.stringify, this function directly // creates a json string manually. () => '{"id":' + id + '}') } function sortLobbies(lobbies){ if (lobbies !== undefined){ let list= [...lobbies]; list.sort((a,b) => a.lobby_name.localeCompare(b.lobby_name)); return list; } // return lobbies; // fallback on reload page in browser, keep undefiend } // ***************************** models *********************************** let ChatRoomsModel = { allRooms: [], knownSubscrIds:[], // to exclude subscribed from public rooms (subscribedRooms filled to late) subscribedRooms: {}, loadPublicRooms() { // TODO: this doesn't preserve id of rooms, // use regex on response to extract ids. rs.rsJsonApiRequest('/rsMsgs/getListOfNearbyChatLobbies', {}, data => ChatRoomsModel.allRooms = sortLobbies(data.public_lobbies), ); }, loadSubscribedRooms(after = null) { // ChatRoomsModel.subscribedRooms = {}; rs.rsJsonApiRequest('/rsMsgs/getChatLobbyList', {}, // JS uses double precision numbers of 64 bit. It is equivalent // to 53 bits of precision. All large precision ints will // get truncated to an approximation. // This API uses Cpp-style 64 bits for `id`. // So we use the string-value 'xstr64' instead data => { let ids = data.cl_list.map(lid => lid.xstr64); ChatRoomsModel.knownSubscrIds = ids; let rooms = {}; ids.map(id => loadLobbyDetails(id, info => { rooms[id]= info; if (Object.keys(rooms).length===ids.length) { // apply rooms to subscribedRooms only after reading all room-details, so sorting all or nothing ChatRoomsModel.subscribedRooms = rooms; } })); if (after != null) { after() } }, ) }, subscribed(info) { return this.knownSubscrIds.includes(info.lobby_id.xstr64); }, }; const ChatLobbyModel = { currentLobby: { lobby_name: '...', }, lobby_user: '...', isSubscribed: false, messages: [], users: [], setupAction: (lobby_id, nick) => {}, setIdentity(lobby_id, nick) { rs.rsJsonApiRequest( '/rsMsgs/setIdentityForChatLobby', {}, () => m.route.set('/chat/:lobby_id',{lobby_id: lobby_id}), true, {}, JSON.parse, ()=> '{"lobby_id":' + lobby_id + ',"nick":"' + nick + '"}' ); }, enterPublicLobby(lobby_id, nick) { console.info('joinVisibleChatLobby', nick, '@', lobby_id) rs.rsJsonApiRequest( '/rsMsgs/joinVisibleChatLobby', {}, () => { loadLobbyDetails(lobby_id, info => { ChatRoomsModel.subscribedRooms[lobby_id]= info; ChatRoomsModel.loadSubscribedRooms(() => { m.route.set('/chat/:lobby', { lobby: info.lobby_id.xstr64 }); }) }); }, true, {}, JSON.parse, ()=> '{"lobby_id":' + lobby_id + ',"own_id":"' + nick + '"}' ); }, unsubscribeChatLobby(lobby_id, follow) { console.info('unsubscribe lobby', lobby_id) rs.rsJsonApiRequest( '/rsMsgs/unsubscribeChatLobby', {}, () => ChatRoomsModel.loadSubscribedRooms(follow), true, {}, JSON.parse, () => '{"lobby_id":' + lobby_id + '}' ) }, chatId(action) { return {type:3,lobby_id:{xstr64:m.route.param('lobby')}}; }, loadLobby (currentlobbyid) { loadLobbyDetails(currentlobbyid, detail => { this.setupAction= this.setIdentity; this.currentLobby = detail; this.isSubscribed = true; this.lobby_user = rs.userList.username(detail.gxs_id) || '???'; let lobbyid = currentlobbyid; // apply existing messages to current lobby view rs.events[15].chatMessages(this.chatId(),rs.events[15], l => (this.messages = l.map(msg=> m(Message, msg)))); // register for chatEvents for future messages rs.events[15].notify = chatMessage => { if (chatMessage.chat_id.type===3 && chatMessage.chat_id.lobby_id.xstr64 === lobbyid) { this.messages.push(m(Message,chatMessage)); m.redraw(); } } // lookup for chat-user names (only snapshot, we don't get notified about changes of participants) var names = detail.gxs_ids.reduce((a,u) => a.concat(rs.userList.username(u.key)), []); names.sort((a,b) => a.localeCompare(b)); this.users = []; names.forEach(name => this.users = this.users.concat([m('.user',name)])); return this.users; }); }, loadPublicLobby (currentlobbyid) { console.info('loadPublicLobby ChatRoomsModel:',ChatRoomsModel); this.setupAction= this.enterPublicLobby; this.isSubscribed = false; ChatRoomsModel.allRooms.forEach(it => { if (it.lobby_id.xstr64 === currentlobbyid) { this.currentLobby = it; this.lobby_user = '???'; this.lobbyid = currentlobbyid; } }) this.users = [] }, sendMessage(msg, onsuccess) { rs.rsJsonApiRequest('/rsmsgs/sendChat', {}, () => { // adding own message to log rs.events[15].handler({ mChatMessage:{ chat_id:this.chatId(), msg:msg, sendTime:new Date().getTime()/1000, lobby_peer_gxs_id:this.currentLobby.gxs_id, } },rs.events[15]); onsuccess(); }, true, {}, undefined, () => '{"id":{"type": 3,"lobby_id":' + m.route.param('lobby') + '}, "msg":' + JSON.stringify(msg) + '}' ); }, selected(info, selName, defaultName) { let currid = (ChatLobbyModel.currentLobby.lobby_id || {xstr64: m.route.param('lobby')}).xstr64 return ((info.lobby_id.xstr64 === currid) ? selName : '') + defaultName; }, switchToEvent(info) { return () => { ChatLobbyModel.currentLobby = info; m.route.set('/chat/:lobby', { lobby: info.lobby_id.xstr64 }); ChatLobbyModel.loadLobby(info.lobby_id.xstr64); // update }; }, setupEvent(info) { return () => { m.route.set('/chat/:lobby/setup', { lobby: info.lobby_id.xstr64 }); ChatLobbyModel.loadPublicLobby(info.lobby_id.xstr64); // update }; } } // ************************* views **************************** /** * Message displays a single Chat-Message
* currently removes formatting and in consequence inline links * msg: Message to Display */ const Message = () => { let msg = null; // message to display let text = ''; // extracted text to display let datetime = ''; // date time to display let username = ''; // username to display (later may be linked) return { oninit: vnode => { console.info('chat Message',vnode); msg = vnode.attrs; datetime = new Date(msg.sendTime * 1000).toLocaleTimeString(); username = rs.userList.username(msg.lobby_peer_gxs_id); text = msg.msg.replaceAll('
','\n').replace(new RegExp('|<[^>]*>','gm'),''); console.info('chat Text', text); }, view: () => m('.message', m('span.datetime', datetime), m('span.username', username), m('span.messagetext', text)) }; } const Lobby = () => { let info = {}; let tagname = ''; let onclick = e => {}; let lobbytagname = ''; return { oninit: (v) => { info = v.attrs.info; tagname = v.attrs.tagname; onclick = v.attrs.onclick || (e => {}); lobbytagname= v.attrs.lobbytagname || 'mainname'; }, view: v => { return m(ChatLobbyModel.selected(info, '.selected-lobby', tagname), { key: info.lobby_id.xstr64, onclick: onclick, }, [ m('h5', {class: lobbytagname},info.lobby_name === '' ? '' : info.lobby_name), m('.topic', info.lobby_topic), ]); }, }; }; const LobbyList = { view(vnode) { let tagname = vnode.attrs.tagname let lobbytagname = vnode.attrs.lobbytagname let onclick = vnode.attrs.onclick||(() => null) return [ m('hr'), vnode.attrs.rooms.map(info => m(Lobby, { info, tagname, lobbytagname, onclick: onclick(info), })) ]; }, } const SubscribedLeftLobbies = { view() { return [ m('h5.lefttitle', 'subscribed:'), m(LobbyList, { rooms: sortLobbies(Object.values(ChatRoomsModel.subscribedRooms)), tagname: '.leftlobby.subscribed', lobbytagname:'leftname', onclick: ChatLobbyModel.switchToEvent, }) ] } }; const SubscribedLobbies = { view() { return m('.widget', [ m('h3', 'Subscribed chat rooms'), m(LobbyList, { rooms: sortLobbies(Object.values(ChatRoomsModel.subscribedRooms)), tagname:'.lobby.subscribed', onclick: ChatLobbyModel.switchToEvent, }), ]) } } const PublicLeftLobbies = { view() { return [ m('h5.lefttitle', 'public:'), m(LobbyList, { rooms: Object.values(ChatRoomsModel.allRooms).filter(info => !ChatRoomsModel.subscribed(info)), tagname: '.leftlobby.public', lobbytagname: 'leftname', onclick: ChatLobbyModel.setupEvent, }), ] } } const PublicLobbies = () => { return m('.widget', [ m('h3', 'Public chat rooms'), m(LobbyList, { rooms: ChatRoomsModel.allRooms.filter(info => !ChatRoomsModel.subscribed(info)), tagname: '.lobby.public', onclick: ChatLobbyModel.setupEvent, }), ]) }; const LobbyName = () => { return m('h3.lobbyName', ChatLobbyModel.isSubscribed ? [ m('span.chatusername',ChatLobbyModel.lobby_user), m('span.chatatchar','@'), ] : [], m('span.chatlobbyname',ChatLobbyModel.currentLobby.lobby_name), m.route.param('subaction') !== 'setup' ? [ m('i.fas.fa-cog.setupicon', { title: 'configure lobby', onclick:() => m.route.set('/chat/:lobby/:subaction', { lobby: m.route.param('lobby'), subaction: 'setup' }, { replace: true }) }) ] : [], ChatLobbyModel.isSubscribed ? [ m('i.fas.fa-sign-out-alt.leaveicon', { title: 'leaving lobby', onclick:() => ChatLobbyModel.unsubscribeChatLobby(m.route.param('lobby'), () => { m.route.set('/chat', null, { replace: true }); }) }) ] : [] ); }; // ***************************** Page Layouts ****************************** const Layout = () => { return { view: () => m('.tab-page', [ m(SubscribedLobbies), PublicLobbies(), ]) } }; const LayoutSingle = () => { return { oninit: () => ChatLobbyModel.loadLobby(m.route.param('lobby')), view: vnode => m('.tab-page', [ LobbyName(), m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies), ), m('.messages', ChatLobbyModel.messages), m('.rightbar', ChatLobbyModel.users), m('.chatMessage', {}, m("textarea.chatMsg", { placeholder: 'enter new message and press return to send', onkeydown: e => { if (e.code==='Enter') { var msg = e.target.value; e.target.value = ' sending ... '; ChatLobbyModel.sendMessage(msg, () => e.target.value=''); return false; } } }) ), ]), }; }; const LayoutSetup = () => { let ownIds = []; return { oninit: () => people_util.ownIds(data => ownIds = data), view: vnode => m('.tab-page', [ LobbyName(), m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies), ), m('.setup', [ m('h5.selectidentity','Select identity to use'), ownIds.map(nick => m( '.identity' + (ChatLobbyModel.currentLobby.gxs_id === nick ? '.selectedidentity' : ''), { onclick: () => ChatLobbyModel.setupAction(m.route.param('lobby'), nick) }, rs.userList.username(nick), )), ]), ]), }; }; /* /rsMsgs/initiateDistantChatConnexion * @param[in] to_pid RsGxsId to start the connection * @param[in] from_pid owned RsGxsId who start the connection * @param[out] pid distant chat id * @param[out] error_code if the connection can't be stablished * @param[in] notify notify remote that the connection is stablished */ const LayoutCreateDistant = () => { let ownIds = []; return { oninit: () => people_util.ownIds(data => ownIds = data), view: vnode => m('.tab-page', [ m('.createDistantChat', [ 'choose identitiy to chat with ', rs.userList.username(m.route.param('lobby')), ownIds.map(id => m('.identity', { onclick: () => rs.rsJsonApiRequest( '/rsMsgs/initiateDistantChatConnexion',{ to_pid:m.route.param('lobby'), from_pid:id, notify: true }, result => { console.info('initiateDistantChatConnexion', result); m.route.set('/chat/:lobbyid',{lobbyid:result.pid}); } ) }, rs.userList.username(id))), ]), ]), }; }; module.exports = { oninit: () => { ChatRoomsModel.loadSubscribedRooms(); ChatRoomsModel.loadPublicRooms(); }, view: (vnode) => { if (m.route.param('lobby') === undefined) { return m(Layout); } else if (m.route.param('subaction') === 'setup') { return m(LayoutSetup); } else if (m.route.param('subaction') === 'createdistantchat') { return m(LayoutCreateDistant); } else { return m(LayoutSingle); } } };