Integrate searching public rooms and people into the new search experience (#8707)
* Implement searching for public rooms and users in new search experience * Implement loading indicator for spotlight results * Moved spotlight dialog into own subfolder * Extract search result avatar into separate component * Build generic new dropdown menu component * Build new network menu based on new network dropdown component * Switch roomdirectory to use new network dropdown * Replace old networkdropdown with new networkdropdown * Added component for public room result details * Extract hooks and subcomponents from SpotlightDialog * Create new hook to get profile info based for an mxid * Add hook to automatically re-request search results * Add hook to prevent out-of-order search results * Extract member sort algorithm from InviteDialog * Keep sorting for non-room results stable * Sort people suggestions using sort algorithm from InviteDialog * Add copy/copied tooltip for invite link option in spotlight * Clamp length of topic for public room results * Add unit test for useDebouncedSearch * Add unit test for useProfileInfo * Create cypress test cases for spotlight dialog * Add test for useLatestResult to prevent out-of-order results
This commit is contained in:
parent
37298d7b1b
commit
5096e7b992
38 changed files with 3520 additions and 1397 deletions
110
src/utils/SortMembers.ts
Normal file
110
src/utils/SortMembers.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { groupBy, mapValues, maxBy, minBy, sumBy, takeRight } from "lodash";
|
||||
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { Member } from "./direct-messages";
|
||||
import DMRoomMap from "./DMRoomMap";
|
||||
import { compare } from "./strings";
|
||||
|
||||
export const compareMembers = (
|
||||
activityScores: Record<string, IActivityScore>,
|
||||
memberScores: Record<string, IMemberScore>,
|
||||
) => (a: Member | RoomMember, b: Member | RoomMember): number => {
|
||||
const aActivityScore = activityScores[a.userId]?.score ?? 0;
|
||||
const aMemberScore = memberScores[a.userId]?.score ?? 0;
|
||||
const aScore = aActivityScore + aMemberScore;
|
||||
const aNumRooms = memberScores[a.userId]?.numRooms ?? 0;
|
||||
|
||||
const bActivityScore = activityScores[b.userId]?.score ?? 0;
|
||||
const bMemberScore = memberScores[b.userId]?.score ?? 0;
|
||||
const bScore = bActivityScore + bMemberScore;
|
||||
const bNumRooms = memberScores[b.userId]?.numRooms ?? 0;
|
||||
|
||||
if (aScore === bScore) {
|
||||
if (aNumRooms === bNumRooms) {
|
||||
return compare(a.userId, b.userId);
|
||||
}
|
||||
|
||||
return bNumRooms - aNumRooms;
|
||||
}
|
||||
return bScore - aScore;
|
||||
};
|
||||
|
||||
function joinedRooms(cli: MatrixClient): Room[] {
|
||||
return cli.getRooms()
|
||||
.filter(r => r.getMyMembership() === 'join')
|
||||
// Skip low priority rooms and DMs
|
||||
.filter(r => !DMRoomMap.shared().getUserIdForRoomId(r.roomId))
|
||||
.filter(r => !Object.keys(r.tags).includes("m.lowpriority"));
|
||||
}
|
||||
|
||||
interface IActivityScore {
|
||||
lastSpoke: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
// Score people based on 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".
|
||||
export function buildActivityScores(cli: MatrixClient): { [key: string]: IActivityScore } {
|
||||
const now = new Date().getTime();
|
||||
const earliestAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago
|
||||
const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic
|
||||
const events = joinedRooms(cli)
|
||||
.flatMap(room => takeRight(room.getLiveTimeline().getEvents(), maxMessagesConsidered))
|
||||
.filter(ev => ev.getTs() > earliestAgeConsidered);
|
||||
const senderEvents = groupBy(events, ev => ev.getSender());
|
||||
return mapValues(senderEvents, events => {
|
||||
const lastEvent = maxBy(events, ev => ev.getTs());
|
||||
const distanceFromNow = Math.abs(now - lastEvent.getTs()); // abs to account for slight future messages
|
||||
const inverseTime = (now - earliestAgeConsidered) - distanceFromNow;
|
||||
return {
|
||||
lastSpoke: lastEvent.getTs(),
|
||||
// Scores from being in a room give a 'good' score of about 1.0-1.5, so for our
|
||||
// score we'll try and award at least 1.0 for making the list, with 4.0 being
|
||||
// an approximate maximum for being selected.
|
||||
score: Math.max(1, inverseTime / (15 * 60 * 1000)), // 15min segments to keep scores sane
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
interface IMemberScore {
|
||||
member: RoomMember;
|
||||
score: number;
|
||||
numRooms: number;
|
||||
}
|
||||
|
||||
export function buildMemberScores(cli: MatrixClient): { [key: string]: IMemberScore } {
|
||||
const maxConsideredMembers = 200;
|
||||
const consideredRooms = joinedRooms(cli).filter(room => room.getJoinedMemberCount() < maxConsideredMembers);
|
||||
const memberPeerEntries = consideredRooms
|
||||
.flatMap(room =>
|
||||
room.getJoinedMembers().map(member =>
|
||||
({ member, roomSize: room.getJoinedMemberCount() })));
|
||||
const userMeta = groupBy(memberPeerEntries, ({ member }) => member.userId);
|
||||
return mapValues(userMeta, roomMemberships => {
|
||||
const maximumPeers = maxConsideredMembers * roomMemberships.length;
|
||||
const totalPeers = sumBy(roomMemberships, entry => entry.roomSize);
|
||||
return {
|
||||
member: minBy(roomMemberships, entry => entry.roomSize).member,
|
||||
numRooms: roomMemberships.length,
|
||||
score: Math.max(0, Math.pow(1 - (totalPeers / maximumPeers), 5)),
|
||||
};
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue