diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 9faed6e..f3b70de 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -1202,12 +1202,20 @@ const ChatRoomJoinView = () => { m('.detail-section', [ m('h3', 'Join Room'), m('p.join-description', 'Select an identity to join this chat room:'), + ChatRoomsModel.joiningLobbyId === lobbyHexId && + m('p.join-description', [m('i.fas.fa-spinner.fa-spin'), ' Joining…']), + ChatRoomsModel.joinError && m('p.error', ChatRoomsModel.joinError), m( '.identities-grid', ownIds.map((nick) => m( '.identity-card', - { onclick: () => ChatLobbyModel.enterPublicLobby(lobbyHexId, nick) }, + { + class: ChatRoomsModel.joiningLobbyId === lobbyHexId ? 'disabled' : '', + onclick: () => ChatRoomsModel.invitationIds.has(lobbyHexId) + ? ChatRoomsModel.acceptInvitation(lobbyHexId, nick) + : ChatLobbyModel.enterPublicLobby(lobbyHexId, nick), + }, [ m('.identity-card__identity', [ m(peopleUtil.IdentityAvatar, { @@ -1299,6 +1307,13 @@ const Layout = { .filter((info) => !ChatRoomsModel.subscribed(info)) .filter((info) => (info.lobby_name || '').toLowerCase().includes(search)); + const invitedRooms = publicRooms.filter((info) => + ChatRoomsModel.invitationIds.has(rs.idToHex(info.lobby_id)) + ); + const discoverableRooms = publicRooms.filter((info) => + !ChatRoomsModel.invitationIds.has(rs.idToHex(info.lobby_id)) + ); + const isSelected = (info, type) => ChatHubState.selectedRoomId === rs.idToHex(info.lobby_id); @@ -1396,12 +1411,41 @@ const Layout = { }), ], - publicRooms.length > 0 && [ + invitedRooms.length > 0 && [ + m('.rooms-section-title.invited-rooms-title', [ + m('i.fas.fa-envelope'), + m('span', 'Invitations (' + invitedRooms.length + ')'), + ]), + invitedRooms.map((info) => { + const hexId = rs.idToHex(info.lobby_id); + return m( + '.chat-room-list-item.public-room.invited-room' + + (isSelected(info, 'public') ? '.selected' : ''), + { + key: hexId, + onclick: () => { + ChatHubState.mobilePane = 'detail'; + m.route.set('/chat/:lobby', { lobby: hexId }); + }, + }, + [ + m('.room-icon', m('i.fas.fa-envelope-open-text')), + m('.room-meta', [ + m('.room-name', info.lobby_name || ''), + m('.room-topic', info.lobby_topic || 'You were invited to join'), + ]), + m('.room-badge', { title: 'Chat room invitation' }, '!'), + ] + ); + }), + ], + + discoverableRooms.length > 0 && [ m('.rooms-section-title', [ m('i.fas.fa-globe'), - m('span', 'Public (' + publicRooms.length + ')'), + m('span', 'Public (' + discoverableRooms.length + ')'), ]), - publicRooms.map((info) => { + discoverableRooms.map((info) => { const hexId = rs.idToHex(info.lobby_id); const participantCount = info.total_number_of_peers || 0; return m( @@ -1429,7 +1473,8 @@ const Layout = { ], subscribedRooms.length === 0 && - publicRooms.length === 0 && + invitedRooms.length === 0 && + discoverableRooms.length === 0 && m('p.no-rooms', 'No chat rooms found'), ]), ]), diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js index 9f21943..2cd9011 100644 --- a/webui-src/app/chat/chat_state.js +++ b/webui-src/app/chat/chat_state.js @@ -404,6 +404,51 @@ const ChatRoomsModel = { knownSubscrIds: [], subscribedRooms: {}, unreadCount: {}, + invitationIds: new Set(), + joiningLobbyId: null, + joinError: '', + invitationCount() { + return this.invitationIds.size; + }, + loadPendingInvitations() { + rs.rsJsonApiRequest('/rsChats/getPendingChatLobbyInvites', {}, (data) => { + const invites = data && Array.isArray(data.invites) ? data.invites : []; + const previousInvitationIds = this.invitationIds; + this.invitationIds = new Set(invites.map((invite) => rs.idToHex(invite.lobby_id))); + const inviteIds = this.invitationIds; + const rooms = this.allRooms.filter((room) => { + const id = rs.idToHex(room.lobby_id); + return !previousInvitationIds.has(id) && !inviteIds.has(id); + }); + this.allRooms = sortLobbies([...rooms, ...invites]); + m.redraw(); + }); + }, + receiveAdministrativeEvent(event) { + // RsChatLobbyEventCode::CHAT_LOBBY_INVITE_RECEIVED + if (event && Number(event.mEventCode) === 4) this.loadPendingInvitations(); + }, + acceptInvitation(lobbyId, identity) { + this.joiningLobbyId = lobbyId; + this.joinError = ''; + return rs.rsJsonApiRequest( + '/rsChats/acceptLobbyInvite', + { id: { xstr64: lobbyId }, identity }, + (data, success) => { + this.joiningLobbyId = null; + if (!success || !data || !data.retval) { + this.joinError = 'RetroShare rejected this identity. This room may require a signed identity.'; + m.redraw(); + return; + } + this.invitationIds.delete(lobbyId); + this.loadSubscribedRooms(); + ChatHubState.selectedRoomType = 'subscribed'; + ChatLobbyModel.loadLobby(lobbyId); + m.redraw(); + } + ); + }, loadPublicRooms() { rs.rsJsonApiRequest( '/rsChats/getListOfNearbyChatLobbies', @@ -417,14 +462,24 @@ const ChatRoomsModel = { seen.add(id); return true; }); - ChatRoomsModel.allRooms = sortLobbies(uniqueLobbies); + const inviteIds = ChatRoomsModel.invitationIds; + const pendingInvites = ChatRoomsModel.allRooms.filter((room) => + inviteIds.has(rs.idToHex(room.lobby_id)) + ); + ChatRoomsModel.allRooms = sortLobbies([ + ...uniqueLobbies.filter((room) => !inviteIds.has(rs.idToHex(room.lobby_id))), + ...pendingInvites, + ]); } else { - ChatRoomsModel.allRooms = []; + ChatRoomsModel.allRooms = ChatRoomsModel.allRooms.filter((room) => + ChatRoomsModel.invitationIds.has(rs.idToHex(room.lobby_id)) + ); } } ); }, loadSubscribedRooms(after = null) { + ChatRoomsModel.loadPendingInvitations(); rs.rsJsonApiRequest( '/rsChats/getChatLobbyList', {}, @@ -432,6 +487,7 @@ const ChatRoomsModel = { if (data && data.cl_list) { const ids = [...new Set(data.cl_list.map((lid) => rs.idToHex(lid)))]; ChatRoomsModel.knownSubscrIds = ids; + ids.forEach((id) => ChatRoomsModel.invitationIds.delete(id)); Object.keys(ChatRoomsModel.subscribedRooms).forEach((id) => { if (!ids.includes(id)) { @@ -886,6 +942,8 @@ const ChatLobbyModel = { ); }, enterPublicLobby(lobbyId, nick) { + ChatRoomsModel.joiningLobbyId = lobbyId; + ChatRoomsModel.joinError = ''; rs.rsJsonApiRequest( '/rsChats/joinVisibleChatLobby', { @@ -893,7 +951,21 @@ const ChatLobbyModel = { own_id: nick, }, (data, success) => { - if (!success || !data || !data.retval) return; + ChatRoomsModel.joiningLobbyId = null; + if (!success || !data || !data.retval) { + const room = ChatHubState.selectedRoom || {}; + const flags = Number(room.lobby_flags || 0); + if ((flags & 0x10) !== 0) { + ChatRoomsModel.joinError = 'This room requires a signed identity. Select a PGP-linked identity.'; + } else if (Number(room.total_number_of_peers || 0) === 0) { + ChatRoomsModel.joinError = 'This room is no longer being advertised by an online participant. Try again when someone in the room is online.'; + ChatRoomsModel.loadPublicRooms(); + } else { + ChatRoomsModel.joinError = 'RetroShare could not join this room. It may no longer be available; refresh the room list and try again.'; + } + m.redraw(); + return; + } // Keep the subscription in the RetroShare profile so the core joins // this room again after a restart. Recent cores also enable this from @@ -909,13 +981,10 @@ const ChatLobbyModel = { true ); - loadLobbyDetails(lobbyId, (info) => { - if (!info) return; - ChatRoomsModel.subscribedRooms[lobbyId] = info; - ChatRoomsModel.loadSubscribedRooms(() => { - m.route.set('/chat/:lobby', { lobby: rs.idToHex(info.lobby_id) }); - }); - }); + ChatRoomsModel.loadSubscribedRooms(); + ChatHubState.selectedRoomType = 'subscribed'; + ChatLobbyModel.loadLobby(lobbyId); + m.redraw(); }, true ); diff --git a/webui-src/app/main.js b/webui-src/app/main.js index 78c46cd..7bd88e9 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -24,7 +24,9 @@ const sumCounts = (counts) => Object.values(counts || {}) function navigationCount(name) { if (name === 'network') return sumCounts(networkState.State.unreadChatCount); if (name === 'people') return sumCounts(peopleState.State.unreadChatCount); - if (name === 'chat') return sumCounts(ChatRoomsModel.unreadCount); + if (name === 'chat') { + return sumCounts(ChatRoomsModel.unreadCount) + ChatRoomsModel.invitationCount(); + } if (name === 'mail') return mail.Messages.unreadCount(); return 0; } @@ -325,10 +327,14 @@ const Layout = () => { rs.events[eventType].notify = () => mail.Messages.refreshSoon(); }); if (!rs.events[15]) return; - rs.events[15].notify = (message) => { - networkState.receiveDirectChatMessage(message); - peopleState.receiveDistantChatMessage(message); - receiveLobbyChatMessage(message); + rs.events[15].notify = (messageOrEvent) => { + if (messageOrEvent && messageOrEvent.mEventCode !== undefined) { + ChatRoomsModel.receiveAdministrativeEvent(messageOrEvent); + return; + } + networkState.receiveDirectChatMessage(messageOrEvent); + peopleState.receiveDistantChatMessage(messageOrEvent); + receiveLobbyChatMessage(messageOrEvent); }; }, view: (vnode) => diff --git a/webui-src/app/rswebui.js b/webui-src/app/rswebui.js index 376cdb1..a8003e5 100644 --- a/webui-src/app/rswebui.js +++ b/webui-src/app/rswebui.js @@ -265,9 +265,9 @@ const eventQueue = { r.push(event.mChatMessage); owner.notify(event.mChatMessage); }); - } else if (event && event.mCid) { + } else if (event && (event.mCid || event.mEventCode !== undefined)) { // Administrative chat event (e.g. lobby info change, peer join/leave) - // Silent for now to avoid console spam, as actual messages use mChatMessage + owner.notify(event); } }, notify: () => { },