Fixed to use counters badges only for new chats

This commit is contained in:
defnax 2026-08-27 13:06:22 +02:00
parent f716a61298
commit 97a4dd5578
12 changed files with 368 additions and 109 deletions

View File

@ -1227,6 +1227,7 @@ const Layout = {
},
onremove: () => {
ChatLobbyModel.stopStatusPolling();
ChatLobbyModel.stopParticipantPolling();
window.removeEventListener('click', Layout.dismissMenu);
},
view: () => {
@ -1314,20 +1315,6 @@ const Layout = {
]),
subscribedRooms.map((info) => {
const hexId = rs.idToHex(info.lobby_id);
let count = 0;
let hasOwn = false;
if (info.gxs_ids) {
if (Array.isArray(info.gxs_ids)) {
count = info.gxs_ids.length;
hasOwn = info.gxs_ids.some((u) => u.key === info.gxs_id);
} else if (typeof info.gxs_ids === 'object') {
count = Object.keys(info.gxs_ids).length;
hasOwn = info.gxs_ids[info.gxs_id] !== undefined;
}
}
if (!hasOwn && info.gxs_id && info.gxs_id !== '00000000000000000000000000000000') {
count++;
}
return m(
'.chat-room-list-item' +
(isSelected(info, 'subscribed') ? '.selected' : ''),
@ -1344,7 +1331,8 @@ const Layout = {
m('.room-name', info.lobby_name || '<unnamed>'),
m('.room-topic', info.lobby_topic || 'No topic'),
]),
count > 0 && m('.room-badge', count),
(ChatRoomsModel.unreadCount[hexId] || 0) > 0
&& m('.room-badge', ChatRoomsModel.unreadCount[hexId]),
]
);
}),
@ -1357,7 +1345,7 @@ const Layout = {
]),
publicRooms.map((info) => {
const hexId = rs.idToHex(info.lobby_id);
const count = info.total_number_of_peers || 0;
const participantCount = info.total_number_of_peers || 0;
return m(
'.chat-room-list-item.public-room' +
(isSelected(info, 'public') ? '.selected' : ''),
@ -1374,7 +1362,9 @@ const Layout = {
m('.room-name', info.lobby_name || '<unnamed>'),
m('.room-topic', info.lobby_topic || 'No topic'),
]),
count > 0 && m('.room-badge', count),
participantCount > 0 && m('.room-badge', {
title: `${participantCount} participant${participantCount === 1 ? '' : 's'}`,
}, participantCount),
]
);
}),

View File

@ -373,6 +373,7 @@ const ChatRoomsModel = {
allRooms: [],
knownSubscrIds: [],
subscribedRooms: {},
unreadCount: {},
loadPublicRooms() {
rs.rsJsonApiRequest(
'/rsChats/getListOfNearbyChatLobbies',
@ -570,6 +571,76 @@ const ChatLobbyModel = {
lastLobbyId: null,
distantChatStatus: null,
statusPollInterval: null,
participantPollInterval: null,
updateParticipants(detail) {
if (!detail) return;
const byId = new Map();
if (detail.gxs_ids) {
if (Array.isArray(detail.gxs_ids)) {
detail.gxs_ids.forEach((entry) => {
const key = entry && entry.key;
if (key) byId.set(key, {
key,
name: rs.userList.username(key) || key,
lastAct: get64Num(entry.value),
});
});
} else if (typeof detail.gxs_ids === 'object') {
Object.keys(detail.gxs_ids).forEach((key) => byId.set(key, {
key,
name: rs.userList.username(key) || key,
lastAct: get64Num(detail.gxs_ids[key]),
}));
}
}
const ownId = detail.gxs_id;
if (ownId && ownId !== '00000000000000000000000000000000' && !byId.has(ownId)) {
byId.set(ownId, {
key: ownId,
name: rs.userList.username(ownId) || ownId,
lastAct: Math.floor(Date.now() / 1000),
});
}
this.users = Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
},
rememberLiveParticipant(chatMessage) {
const cid = chatMessage && chatMessage.chat_id;
if (!cid || cid.type !== 3 || rs.idToHex(cid.lobby_id) !== this.lastLobbyId) return;
const key = rs.idToHex(chatMessage.lobby_peer_gxs_id || chatMessage.peerId);
if (!key || /^0+$/.test(key)) return;
const existing = this.users.find((user) => user.key === key);
if (existing) {
existing.lastAct = chatMessage.sendTime || Math.floor(Date.now() / 1000);
} else {
this.users.push({
key,
name: rs.userList.username(key) || chatMessage.peerName || key,
lastAct: chatMessage.sendTime || Math.floor(Date.now() / 1000),
});
}
},
startParticipantPolling(lobbyId) {
this.stopParticipantPolling();
const refresh = () => loadLobbyDetails(lobbyId, (detail) => {
if (!detail || this.lastLobbyId !== lobbyId) return;
this.currentLobby = { ...this.currentLobby, ...detail, chatType: 3 };
this.updateParticipants(detail);
m.redraw();
});
refresh();
this.participantPollInterval = setInterval(refresh, 5000);
},
stopParticipantPolling() {
if (this.participantPollInterval) {
clearInterval(this.participantPollInterval);
this.participantPollInterval = null;
}
},
pollDistantChatStatus() {
if (!this.currentLobby || this.currentLobby.chatType !== 2) return;
@ -799,7 +870,9 @@ const ChatLobbyModel = {
},
loadLobby(currentlobbyid) {
this.stopStatusPolling();
this.stopParticipantPolling();
this.lastLobbyId = currentlobbyid;
ChatRoomsModel.unreadCount[currentlobbyid] = 0;
const finishLoad = (detail) => {
this.setupAction = this.setIdentity;
@ -817,76 +890,50 @@ const ChatLobbyModel = {
this.addMessages(l);
});
rs.events[15].notify = (chatMessage) => {
const msgCid = chatMessage.chat_id;
let msgId;
if (msgCid.type === 3) {
msgId = rs.idToHex(msgCid.lobby_id);
} else if (msgCid.type === 2) {
msgId = rs.idToHex(msgCid.distant_chat_id);
} else if (msgCid.type === 1) {
msgId = rs.idToHex(msgCid.peer_id);
} else {
msgId = rs.idToHex(msgCid);
}
if (msgId === currentlobbyid) {
this.addMessages([chatMessage]);
}
};
let list = [];
if (detail.gxs_ids) {
if (Array.isArray(detail.gxs_ids)) {
list = detail.gxs_ids.map((u) => {
const key = u.key;
return { key, name: rs.userList.username(key) || key, lastAct: get64Num(u.value) };
});
} else if (typeof detail.gxs_ids === 'object') {
list = Object.keys(detail.gxs_ids).map((key) => {
return { key, name: rs.userList.username(key) || key, lastAct: get64Num(detail.gxs_ids[key]) };
});
}
}
const ownId = detail.gxs_id;
if (ownId && ownId !== '00000000000000000000000000000000') {
const hasOwn = list.some((u) => u.key === ownId);
if (!hasOwn) {
list.push({
key: ownId,
name: rs.userList.username(ownId) || ownId,
lastAct: Math.floor(Date.now() / 1000)
});
}
}
if (list.length === 0) {
list = [{ key: ownId || '', name: rs.userList.username(ownId) || detail.lobby_name || '???', lastAct: Math.floor(Date.now() / 1000) }];
}
list.sort((a, b) => a.name.localeCompare(b.name));
this.users = list;
this.updateParticipants(detail);
if (detail.chatType === 2) {
this.startStatusPolling();
} else if (detail.chatType === 3) {
this.startParticipantPolling(currentlobbyid);
}
m.redraw();
};
loadLobbyDetails(currentlobbyid, (detail) => {
const isDistantChatId = /^[0-9a-f]{32}$/i.test(String(currentlobbyid));
const loadDetails = (attempt = 0) => loadLobbyDetails(currentlobbyid, (detail) => {
if (detail) {
finishLoad(detail);
} else {
return;
}
// Public lobby IDs are uint64 decimal strings. Passing one to the
// distant-chat fallback makes the core construct a 128-bit tunnel ID
// from (for example) a 20-character decimal value and can terminate the
// JSON API listener. Only a real 32-hex-character tunnel ID may use it.
if (isDistantChatId) {
loadDistantChatDetails(currentlobbyid, (dDetail) => {
if (dDetail) {
finishLoad(dDetail);
}
if (dDetail) finishLoad(dDetail);
});
return;
}
// A newly joined room may not be immediately visible through
// getChatLobbyInfo. Prefer the lobby data already loaded by the room
// lists, then retry briefly while the core completes the subscription.
const cached = ChatRoomsModel.subscribedRooms[currentlobbyid]
|| (ChatRoomsModel.allRooms || []).find(
(room) => rs.idToHex(room.lobby_id) === currentlobbyid
);
if (cached) {
finishLoad({ ...cached, chatType: 3 });
} else if (attempt < 3) {
setTimeout(() => loadDetails(attempt + 1), 250 * (attempt + 1));
}
});
loadDetails();
},
loadPublicLobby(currentlobbyid) {
this.setupAction = this.enterPublicLobby;
@ -993,6 +1040,22 @@ const ChatHubState = {
},
};
function receiveLobbyChatMessage(chatMessage) {
const cid = chatMessage && chatMessage.chat_id;
if (!cid || cid.type !== 3) return;
const lobbyId = rs.idToHex(cid.lobby_id);
if (!lobbyId) return;
ChatLobbyModel.rememberLiveParticipant(chatMessage);
const isOpen = m.route.get().split('/')[1] === 'chat'
&& ChatLobbyModel.lastLobbyId === lobbyId
&& (window.innerWidth > 700 || ChatHubState.mobilePane === 'detail');
if (isOpen) ChatLobbyModel.addMessages([chatMessage]);
else if (chatMessage.incoming === true) {
ChatRoomsModel.unreadCount[lobbyId] = (ChatRoomsModel.unreadCount[lobbyId] || 0) + 1;
m.redraw();
}
}
module.exports = {
get64Num,
loadLobbyDetails,
@ -1009,4 +1072,5 @@ module.exports = {
Message,
ChatLobbyModel,
ChatHubState,
receiveLobbyChatMessage,
};

View File

@ -19,6 +19,27 @@ const Messages = {
personal: [],
todo: [],
later: [],
refreshTimer: null,
unreadCount() {
return (Messages.inbox || []).filter((msg) => {
const status = msg.msgflags & 0xf0;
return (status === util.RS_MSG_NEW || status === util.RS_MSG_UNREAD_BY_USER)
&& !(msg.msgflags & util.RS_MSG_TRASH)
&& !(msg.msgflags & util.RS_MSG_SPAM);
}).length;
},
refreshSoon() {
if (Messages.refreshTimer) return;
Messages.refreshTimer = setTimeout(() => {
Messages.refreshTimer = null;
Messages.load();
}, 250);
},
markReadLocally(msgId) {
Messages.all.forEach((msg) => {
if (msg.msgId === msgId) msg.msgflags &= ~0xf0;
});
},
load() {
rs.rsJsonApiRequest('/rsMail/getMessageSummaries', { box: util.BOX_ALL }, (data) => {
if (data && data.msgList) {
@ -61,6 +82,7 @@ const Messages = {
Messages.later = Messages.all.filter(
(msg) => msg.msgtags && msg.msgtags.includes(util.RS_MSGTAGTYPE_LATER)
);
m.redraw();
}
});
},
@ -247,6 +269,10 @@ const GenericMailList = () => {
key: msg.msgId,
details: msg,
category,
onOpen: () => {
Messages.markReadLocally(msg.msgId);
m.redraw();
},
})
)
)
@ -258,6 +284,7 @@ const GenericMailList = () => {
};
module.exports = {
Messages,
view: ({ attrs, attrs: { tab, msgId } }) => {
// TODO: utilize multiple routing params
if (Object.prototype.hasOwnProperty.call(attrs, 'msgId')) {

View File

@ -42,6 +42,16 @@ const MailHoverState = {
hoveredUser: null,
};
function markMessageRead(msgId, onDone) {
rs.rsJsonApiRequest(
'/rsMail/MessageRead',
{ msgId, unreadByUser: false },
(data, success) => {
if (onDone) onDone(Boolean(success && (!data || data.retval !== false)));
}
);
}
function renderMailUserTooltip() {
if (!MailHoverState.hoveredUser) return null;
const hUser = MailHoverState.hoveredUser;
@ -160,8 +170,10 @@ const MessageSummary = () => {
{
key: v.attrs.details.msgId,
class: msgStatus,
onclick: () =>
m.route.set('/mail/:tab/:msgId', { tab: v.attrs.category, msgId: v.attrs.details.msgId }),
onclick: () => {
if (v.attrs.onOpen) v.attrs.onOpen();
m.route.set('/mail/:tab/:msgId', { tab: v.attrs.category, msgId: v.attrs.details.msgId });
},
},
[
m(
@ -326,6 +338,7 @@ const MessageView = () => {
return {
oninit: async (v) => {
markMessageRead(v.attrs.msgId);
const res = await rs.rsJsonApiRequest('/rsMail/getMessage', {
msgId: v.attrs.msgId,
});
@ -808,4 +821,5 @@ module.exports = {
RS_MSGTAGTYPE_TODO,
RS_MSGTAGTYPE_WORK,
BOX_ALL,
markMessageRead,
};

View File

@ -14,6 +14,20 @@ const boards = require('boards/boards');
const config = require('config/config_resolver');
const statistics = require('statistics/statistics');
const statusbar = require('statusbar');
const networkState = require('network/network_state');
const peopleState = require('people/people_state');
const { ChatRoomsModel, receiveLobbyChatMessage } = require('chat/chat_state');
const sumCounts = (counts) => Object.values(counts || {})
.reduce((total, count) => total + Number(count || 0), 0);
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 === 'mail') return mail.Messages.unreadCount();
return 0;
}
const navIcon = {
home: 'i.fas.fa-home.sidenav-icon',
@ -62,13 +76,18 @@ const navbar = () => {
m('.nav-menu__box', { style: { flex: 1 } }, [
Object.keys(vnode.attrs.links).map((linkName) => {
const active = m.route.get().split('/')[1] === linkName;
const count = navigationCount(linkName);
return m(
m.route.Link,
{
href: vnode.attrs.links[linkName],
class: (active ? 'active-link' : '') + ' item',
},
[m(navIcon[linkName]), m('span', linkName.charAt(0).toUpperCase() + linkName.slice(1))]
[
m(navIcon[linkName]),
m('span', linkName.charAt(0).toUpperCase() + linkName.slice(1)),
count > 0 && m('b.nav-unread-badge', count),
]
);
}),
m(
@ -253,7 +272,11 @@ const MobileNavigation = () => {
href,
class: `mobile-bottom-nav__item${routeName() === name ? ' active' : ''}`,
onclick: () => (isMoreOpen = false),
}, [m(navIcon[name]), m('span', name.charAt(0).toUpperCase() + name.slice(1))]);
}, [
m(navIcon[name]),
m('span', name.charAt(0).toUpperCase() + name.slice(1)),
navigationCount(name) > 0 && m('b.nav-unread-badge', navigationCount(name)),
]);
return {
view: () => [
@ -290,6 +313,24 @@ const MobileNavigation = () => {
const Layout = () => {
return {
oninit: () => {
mail.Messages.load();
[rs.RsEventsType.MAIL_STATUS, rs.RsEventsType.MAIL_TAG].forEach((eventType) => {
if (!rs.events[eventType]) {
rs.events[eventType] = {
handler: (event, owner) => owner.notify(event),
notify: () => {},
};
}
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);
};
},
view: (vnode) =>
m('.content', [
m(navbar, {

View File

@ -1,5 +1,4 @@
const m = require('mithril');
const rs = require('rswebui');
const Data = require('network/network_data');
const compose = require('mail/mail_compose');
const {
@ -30,11 +29,6 @@ const NetworkLayout = () => {
loadOwnProfile();
loadGxsIdentities();
},
onremove: () => {
if (rs.events[15]) {
rs.events[15].notify = () => {};
}
},
view: () => {
const selectedFriend = State.selectedFriendGpgId
? Data.gpgDetails[State.selectedFriendGpgId]

View File

@ -251,8 +251,7 @@ function preloadNetworkChatHistory() {
});
}
function loadDirectChatMessages() {
rs.events[15].notify = (chatMessage) => {
function receiveDirectChatMessage(chatMessage) {
const messagePeerId = chatMessage.chat_id && chatMessage.chat_id.peer_id
? rs.idToHex(chatMessage.chat_id.peer_id)
: '';
@ -275,6 +274,7 @@ function loadDirectChatMessages() {
};
const isOpenConversation =
m.route.get().split('/')[1] === 'network' &&
State.activeTab === 'chat' &&
State.selectedFriendGpgId === gpgId &&
normalizedPeerId === String(State.currentChatPeerId || '').toLowerCase() &&
@ -289,7 +289,11 @@ function loadDirectChatMessages() {
scrollChatToBottom();
}
m.redraw();
};
}
function loadDirectChatMessages() {
// Kept for older callers. Incoming messages are now dispatched globally by
// main.js so counters continue to work while another page is open.
}
function markDirectChatRead(gpgId) {
@ -440,6 +444,7 @@ module.exports = {
getOnlineSslId,
preloadNetworkChatHistory,
loadDirectChatMessages,
receiveDirectChatMessage,
markDirectChatRead,
loadRecentDirectChatHistory,
loadAllDirectChatHistory,

View File

@ -15,7 +15,7 @@ const {
initializeDistantChat,
getDistantChatSession,
drainBufferedChatMessages,
receiveDistantChatMessage,
markDistantChatRead,
} = require('people/people_state');
const PeopleSidebar = require('people/people_sidebar');
@ -67,9 +67,6 @@ const PeopleLayout = () => {
});
window.addEventListener('click', dismissMenu);
// Register for chatEvents to receive live incoming messages
rs.events[15].notify = receiveDistantChatMessage;
if (State.chatPid && !State.chatDisconnected) {
// Messages received while the tab was unmounted sit in the event
// queue buffer: pick them up before the first redraw.
@ -80,9 +77,6 @@ const PeopleLayout = () => {
}
},
onremove: () => {
if (rs.events[15]) {
rs.events[15].notify = () => {};
}
stopStatusPolling();
if (stopWatchingOwnIds) stopWatchingOwnIds();
window.removeEventListener('click', dismissMenu);
@ -129,6 +123,7 @@ const PeopleLayout = () => {
onclick: () => {
State.activeTab = 'chat';
State.mobilePane = 'detail';
markDistantChatRead(State.selectedId);
initializeDistantChat();
},
},

View File

@ -15,6 +15,8 @@ const {
get64Num,
stopStatusPolling,
initializeDistantChat,
markDistantChatRead,
isDistantChatActive,
} = require('people/people_state');
const LIST_RENDER_CAP = 200;
@ -47,7 +49,8 @@ const PeopleSidebar = () => {
const hist = State.chatHistoryMap[gxsId];
return Boolean(hist && hist.lastMsg && !isSystemMsg(hist.lastMsg));
});
const activeChatsCount = chatPeerIds.length;
const unreadChatsCount = Object.values(State.unreadChatCount || {})
.reduce((total, count) => total + count, 0);
if (State.mainTab === 'people') {
let baseList;
@ -142,7 +145,7 @@ const PeopleSidebar = () => {
[
m('i.fas.fa-comments'),
' Chats',
activeChatsCount > 0 && m('span.segment-badge', activeChatsCount),
unreadChatsCount > 0 && m('span.segment-badge', unreadChatsCount),
]
),
]),
@ -211,6 +214,7 @@ const PeopleSidebar = () => {
const itemEntry = rs.userList.userMap[gxsId];
const itemIsContact = itemEntry && itemEntry.isContact;
const itemIsOwn = State.ownGxsIds.includes(gxsId);
const hasActiveTunnel = isDistantChatActive(gxsId);
const hist = State.chatHistoryMap[gxsId];
const lastTS = hist ? hist.lastTime : (itemDetails ? get64Num(itemDetails.mLastUsageTS) : 0);
@ -231,6 +235,7 @@ const PeopleSidebar = () => {
State.selectedId = gxsId;
State.activeTab = 'chat';
State.mobilePane = 'detail';
markDistantChatRead(gxsId);
initializeDistantChat();
m.redraw();
},
@ -258,8 +263,11 @@ const PeopleSidebar = () => {
}),
m('.status-dot', {
style: {
backgroundColor: itemIsContact || itemIsOwn ? '#22c55e' : '#cbd5e1',
backgroundColor: hasActiveTunnel ? '#22c55e' : '#cbd5e1',
},
title: hasActiveTunnel
? 'Distant chat tunnel active'
: 'Distant chat tunnel inactive',
}),
]),
m('.chat-info', [
@ -268,6 +276,8 @@ const PeopleSidebar = () => {
]),
m('.chat-meta', [
relativeTimeStr && m('.chat-time', relativeTimeStr),
(State.unreadChatCount[gxsId] || 0) > 0
&& m('.chat-unread-badge', State.unreadChatCount[gxsId]),
]),
]
);

View File

@ -12,6 +12,7 @@ const State = {
ownGxsIds: [],
gpgToGxsIdMap: {},
chatHistoryMap: {}, // gxsId -> { lastMsg, lastTime }
unreadChatCount: {}, // gxsId -> new incoming messages not yet opened
showMailCompose: false,
activeTab: 'details',
mobilePane: 'list', // Phone master/detail navigation: 'list' | 'detail'
@ -645,14 +646,7 @@ function leaveDistantChat(closed) {
m.redraw();
}
// Live incoming distant chat message, coming from the rsEvents stream.
function receiveDistantChatMessage(chatMessage) {
const msgCid = chatMessage && chatMessage.chat_id;
if (!msgCid || msgCid.type !== 2) return;
const msgPid = rs.idToHex(msgCid.distant_chat_id);
if (!msgPid) return;
function findDistantChatSession(msgPid) {
let session = null;
let targetGxsId = null;
Object.keys(State.activeDistantChats || {}).forEach((id) => {
@ -670,7 +664,48 @@ function receiveDistantChatMessage(chatMessage) {
session = getDistantChatSession(targetGxsId);
session.pid = msgPid;
}
if (!session) return;
return { session, targetGxsId };
}
function isDistantChatActive(gxsId) {
const session = gxsId && State.activeDistantChats[gxsId];
return Boolean(
session
&& session.pid
&& !session.disconnected
&& session.status
&& session.status.status === 2
);
}
// A tunnel can survive a page reload, while activeDistantChats cannot. Resolve
// its deterministic id against the small set of identities we can actually be
// chatting with, so background messages still reach the People counter.
async function resolveDistantChatPeer(msgPid) {
const ownIds = State.ownGxsIds.length > 0
? State.ownGxsIds
: await peopleUtil.ownIds();
if (State.ownGxsIds.length === 0) State.ownGxsIds = ownIds || [];
const candidates = new Set([
State.selectedId,
...Object.keys(State.chatHistoryMap || {}),
...Object.keys(State.activeDistantChats || {}),
].filter(peopleUtil.isUsableIdentityId));
peopleUtil.contactlist(rs.userList.users || []).forEach((user) => {
if (user && peopleUtil.isUsableIdentityId(user.mGroupId)) candidates.add(user.mGroupId);
});
for (const ownId of ownIds || []) {
for (const peerId of candidates) {
if (peopleUtil.distantChatPid(ownId, peerId) === msgPid) return peerId;
}
}
return null;
}
function recordDistantChatMessage(chatMessage, msgPid, session, targetGxsId) {
if (!session || !targetGxsId) return;
if (!addSessionMessages(session, [chatMessage])) return;
@ -679,6 +714,13 @@ function receiveDistantChatMessage(chatMessage) {
lastMsg: chatMessage.msg || chatMessage.message || '',
lastTime: chatMessage.sendTime || chatMessage.recvTime || Math.floor(Date.now() / 1000),
};
const isOpenConversation = m.route.get().split('/')[1] === 'people'
&& State.activeTab === 'chat'
&& State.selectedId === targetGxsId
&& (window.innerWidth > 700 || State.mobilePane === 'detail');
if (chatMessage.incoming === true && !isOpenConversation) {
State.unreadChatCount[targetGxsId] = (State.unreadChatCount[targetGxsId] || 0) + 1;
}
}
// The view renders State.chatMessages, so it has to point at the session
@ -689,6 +731,32 @@ function receiveDistantChatMessage(chatMessage) {
if (State.selectedId === targetGxsId) scrollChatToBottom();
}
// Live incoming distant chat message, coming from the rsEvents stream.
function receiveDistantChatMessage(chatMessage) {
const msgCid = chatMessage && chatMessage.chat_id;
if (!msgCid || msgCid.type !== 2) return;
const msgPid = rs.idToHex(msgCid.distant_chat_id);
if (!msgPid) return;
const known = findDistantChatSession(msgPid);
if (known.session) {
recordDistantChatMessage(chatMessage, msgPid, known.session, known.targetGxsId);
return;
}
resolveDistantChatPeer(msgPid).then((targetGxsId) => {
if (!targetGxsId) return;
const session = getDistantChatSession(targetGxsId);
session.pid = msgPid;
recordDistantChatMessage(chatMessage, msgPid, session, targetGxsId);
});
}
function markDistantChatRead(gxsId) {
if (gxsId) State.unreadChatCount[gxsId] = 0;
}
// Two /rsHistory/getMessages per known identity, and a node knows hundreds of
// them. Fired all at once they fill the browser's six sockets and the JSON
// API's single service thread, so everything the user is actually waiting for
@ -839,9 +907,11 @@ function preloadAllChatHistory() {
if (pid) tasks.push(historyPreloadTask(gxsId, distantChatIdFor(pid), false));
});
locationIdsOf(gxsId).forEach((sslId) => {
tasks.push(historyPreloadTask(gxsId, privateChatIdFor(sslId), true));
});
// Direct messages are keyed only by an SSL location, not by the GXS
// identity used for a distant chat. Assigning that same SSL history to
// every GXS identity belonging to the PGP friend creates duplicate chat
// rows. Direct chat belongs to Network; this list is keyed by the exact
// GXS identity whose distant tunnel history matched above.
});
if (tasks.length === 0) {
@ -926,9 +996,11 @@ function loadAllHistoryForSelectedPeer(callback) {
module.exports = {
State,
getDistantChatSession,
isDistantChatActive,
addSessionMessages,
drainBufferedChatMessages,
receiveDistantChatMessage,
markDistantChatRead,
scrollChatToBottom,
isSystemMsg,
preloadAllChatHistory,

View File

@ -34,6 +34,7 @@
position: relative;
.item {
position: relative;
margin: 0;
padding: 0.675rem 0.5rem;
width: 10rem;
@ -57,6 +58,20 @@
}
}
.nav-unread-badge {
margin-left: auto;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 0.35rem;
border-radius: 999px;
display: grid;
place-items: center;
background: #ef4444;
color: white;
font-size: 0.7rem;
line-height: 1;
}
.item.item-selected {
color: $primary-light-color;
background-color: color.adjust($primary-light-color, $alpha: -0.85);
@ -106,6 +121,17 @@
& p {
display: none !important;
}
.nav-unread-badge {
display: grid !important;
position: absolute;
top: 0.1rem;
right: -0.15rem;
min-width: 1rem;
height: 1rem;
padding: 0 0.2rem;
font-size: 0.6rem;
}
}
}
@ -401,6 +427,7 @@
z-index: 900;
&__item {
position: relative;
display: flex;
width: 100%;
align-items: center;
@ -434,6 +461,26 @@
font-size: 0.6rem;
text-overflow: ellipsis;
}
.nav-unread-badge {
position: absolute;
top: 0.1rem;
left: calc(50% + 0.45rem);
display: grid;
place-items: center;
min-width: 1.05rem;
height: 1.05rem;
padding: 0 0.22rem;
box-sizing: border-box;
border: 2px solid #ffffff;
border-radius: 999px;
background: #ef4444;
color: #ffffff;
font-size: 0.6rem;
font-weight: 700;
line-height: 1;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25);
}
}
}

File diff suppressed because one or more lines are too long