From 2faa4254baa06e0f4f0ed53d8289cd47fb65eea6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jan 2020 14:36:23 -0700 Subject: [PATCH 1/5] Score users who have recently spoken higher in invite suggestions Fixes https://github.com/vector-im/riot-web/issues/11769 The algorithm should be documented in the diff as comments. --- src/components/views/dialogs/InviteDialog.js | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 1b7a50c084..0c2a6785e8 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -411,6 +411,57 @@ export default class InviteDialog extends React.PureComponent { return scores; }, {}); + // Now that we have scores for being in rooms, boost those people who have sent messages + // recently, as a way to improve the quality of suggestions. We do this by checking every + // room to see who has sent a message in the last few hours, and giving them a score + // which correlates to the freshness of their message. In theory, this results in suggestions + // which are closer to "continue this conversation" rather than "this person exists". + const trueJoinedRooms = client.getRooms().filter(r => r.getMyMembership() === 'join'); + const now = (new Date()).getTime(); + const maxAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago + const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic + const lastSpoke = {}; // userId: timestamp + const lastSpokeMembers = {}; // userId: room member + for (const room of trueJoinedRooms) { + // Skip low priority rooms and DMs + if (Object.keys(room.tags).includes("m.lowpriority") || DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + continue; + } + + const events = room.getLiveTimeline().getEvents(); // timelines are most recent last + for (let i = events.length - 1; i >= Math.max(0, events.length - maxMessagesConsidered); i--) { + const ev = events[i]; + if (ev.getSender() === MatrixClientPeg.get().getUserId() || excludedUserIds.includes(ev.getSender())) { + continue; + } + if (ev.getTs() <= maxAgeConsidered) { + break; // give up: all events from here on out are too old + } + + if (!lastSpoke[ev.getSender()] || lastSpoke[ev.getSender()] < ev.getTs()) { + lastSpoke[ev.getSender()] = ev.getTs(); + lastSpokeMembers[ev.getSender()] = room.getMember(ev.getSender()); + } + } + } + for (const userId in lastSpoke) { + const ts = lastSpoke[userId]; + const member = lastSpokeMembers[userId]; + if (!member) continue; // skip people we somehow don't have profiles for + + // Scores from being in a room give a 'good' score of about 1.0-1.5, so for our + // boost we'll try and award at least +1.0 for making the list, with +4.0 being + // an approximate maximum for being selected. + const distanceFromNow = Math.abs(now - ts); // abs to account for slight future messages + const inverseTime = (now - maxAgeConsidered) - distanceFromNow; + const scoreBoost = Math.max(1, inverseTime / (15 * 60 * 1000)); // 15min segments to keep scores sane + + let record = memberScores[userId]; + if (!record) record = memberScores[userId] = {score: 0}; + record.member = member; + record.score += scoreBoost; + } + const members = Object.values(memberScores); members.sort((a, b) => { if (a.score === b.score) { From 3850377e275d6ccc27cbe38a07586c266010e264 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Jan 2020 14:40:33 -0700 Subject: [PATCH 2/5] Appease the linter --- src/components/views/dialogs/InviteDialog.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 0c2a6785e8..02a96f7f37 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -424,7 +424,8 @@ export default class InviteDialog extends React.PureComponent { const lastSpokeMembers = {}; // userId: room member for (const room of trueJoinedRooms) { // Skip low priority rooms and DMs - if (Object.keys(room.tags).includes("m.lowpriority") || DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId); + if (Object.keys(room.tags).includes("m.lowpriority") || isDm) { continue; } From 551b2907d841e4f0fcb9c58377e950330f00862f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 09:29:33 -0700 Subject: [PATCH 3/5] Fix variable usage and naming --- src/components/views/dialogs/InviteDialog.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 02a96f7f37..251fb62d2c 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -359,8 +359,8 @@ export default class InviteDialog extends React.PureComponent { _buildSuggestions(excludedTargetIds: string[]): {userId: string, user: RoomMember} { const maxConsideredMembers = 200; - const client = MatrixClientPeg.get(); - const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']]; + const myUserId = MatrixClientPeg.get().getUserId(); + const excludedUserIds = [myUserId, SdkConfig.get()['welcomeUserId']]; const joinedRooms = client.getRooms() .filter(r => r.getMyMembership() === 'join') .filter(r => r.getJoinedMemberCount() <= maxConsideredMembers); @@ -418,7 +418,7 @@ export default class InviteDialog extends React.PureComponent { // which are closer to "continue this conversation" rather than "this person exists". const trueJoinedRooms = client.getRooms().filter(r => r.getMyMembership() === 'join'); const now = (new Date()).getTime(); - const maxAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago + const earliestAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic const lastSpoke = {}; // userId: timestamp const lastSpokeMembers = {}; // userId: room member @@ -432,10 +432,10 @@ export default class InviteDialog extends React.PureComponent { const events = room.getLiveTimeline().getEvents(); // timelines are most recent last for (let i = events.length - 1; i >= Math.max(0, events.length - maxMessagesConsidered); i--) { const ev = events[i]; - if (ev.getSender() === MatrixClientPeg.get().getUserId() || excludedUserIds.includes(ev.getSender())) { + if (ev.getSender() === myUserId || excludedUserIds.includes(ev.getSender())) { continue; } - if (ev.getTs() <= maxAgeConsidered) { + if (ev.getTs() <= earliestAgeConsidered) { break; // give up: all events from here on out are too old } @@ -454,7 +454,7 @@ export default class InviteDialog extends React.PureComponent { // boost we'll try and award at least +1.0 for making the list, with +4.0 being // an approximate maximum for being selected. const distanceFromNow = Math.abs(now - ts); // abs to account for slight future messages - const inverseTime = (now - maxAgeConsidered) - distanceFromNow; + const inverseTime = (now - earliestAgeConsidered) - distanceFromNow; const scoreBoost = Math.max(1, inverseTime / (15 * 60 * 1000)); // 15min segments to keep scores sane let record = memberScores[userId]; From 727ca8ba7792b11c337e0460a05530fd41aef69f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 10:04:14 -0700 Subject: [PATCH 4/5] Don't double check ourselves --- src/components/views/dialogs/InviteDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 251fb62d2c..94d4214fd4 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -432,7 +432,7 @@ export default class InviteDialog extends React.PureComponent { const events = room.getLiveTimeline().getEvents(); // timelines are most recent last for (let i = events.length - 1; i >= Math.max(0, events.length - maxMessagesConsidered); i--) { const ev = events[i]; - if (ev.getSender() === myUserId || excludedUserIds.includes(ev.getSender())) { + if (excludedUserIds.includes(ev.getSender())) { continue; } if (ev.getTs() <= earliestAgeConsidered) { From 7c877fb9c4b50935bb3dd8849c8861ec688d02da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 Jan 2020 10:08:35 -0700 Subject: [PATCH 5/5] Reinstate client variable that is actually used --- src/components/views/dialogs/InviteDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 94d4214fd4..703b0b5121 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -359,8 +359,8 @@ export default class InviteDialog extends React.PureComponent { _buildSuggestions(excludedTargetIds: string[]): {userId: string, user: RoomMember} { const maxConsideredMembers = 200; - const myUserId = MatrixClientPeg.get().getUserId(); - const excludedUserIds = [myUserId, SdkConfig.get()['welcomeUserId']]; + const client = MatrixClientPeg.get(); + const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']]; const joinedRooms = client.getRooms() .filter(r => r.getMyMembership() === 'join') .filter(r => r.getJoinedMemberCount() <= maxConsideredMembers);