Merge pull request #6130 from matrix-org/gsouquet/member-list-sort
This commit is contained in:
commit
96f5d3af05
11 changed files with 46 additions and 27 deletions
src
components/views
dialogs
directory
rooms
settings/tabs
integrations
stores
utils
test/components/views/rooms
|
@ -49,6 +49,7 @@ import {mediaFromMxc} from "../../../customisations/Media";
|
|||
import {getAddressType} from "../../../UserAddress";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { compare } from '../../../utils/strings';
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -578,7 +579,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
members.sort((a, b) => {
|
||||
if (a.score === b.score) {
|
||||
if (a.numRooms === b.numRooms) {
|
||||
return a.member.userId.localeCompare(b.member.userId);
|
||||
return compare(a.member.userId, b.member.userId);
|
||||
}
|
||||
|
||||
return b.numRooms - a.numRooms;
|
||||
|
|
|
@ -39,6 +39,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
|
|||
import TextInputDialog from "../dialogs/TextInputDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { compare } from "../../../utils/strings";
|
||||
|
||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||
|
||||
|
@ -187,7 +188,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
|||
|
||||
protocolsList.forEach(({instances=[]}) => {
|
||||
[...instances].sort((b, a) => {
|
||||
return a.desc.localeCompare(b.desc);
|
||||
return compare(a.desc, b.desc);
|
||||
}).forEach(({desc, instance_id: instanceId}) => {
|
||||
entries.push(
|
||||
<MenuItemRadio
|
||||
|
|
|
@ -238,6 +238,8 @@ export default class MemberList extends React.Component {
|
|||
member.user = cli.getUser(member.userId);
|
||||
}
|
||||
|
||||
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
|
||||
|
||||
// XXX: this user may have no lastPresenceTs value!
|
||||
// the right solution here is to fix the race rather than leave it as 0
|
||||
});
|
||||
|
@ -252,6 +254,8 @@ export default class MemberList extends React.Component {
|
|||
m.membership === 'join' || m.membership === 'invite'
|
||||
);
|
||||
});
|
||||
const language = SettingsStore.getValue("language");
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
}
|
||||
|
@ -351,13 +355,7 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, "");
|
||||
const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, "");
|
||||
// console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
|
||||
return nameA.localeCompare(nameB, {
|
||||
ignorePunctuation: true,
|
||||
sensitivity: "base",
|
||||
});
|
||||
return this.collator.compare(memberA.sortName, memberB.sortName);
|
||||
};
|
||||
|
||||
onSearchQueryChanged = searchQuery => {
|
||||
|
@ -422,7 +420,7 @@ export default class MemberList extends React.Component {
|
|||
} else {
|
||||
// Is a 3pid invite
|
||||
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -484,10 +482,10 @@ export default class MemberList extends React.Component {
|
|||
if (this._getChildCountInvited() > 0) {
|
||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
}
|
||||
|
||||
const footer = (
|
||||
|
@ -520,9 +518,9 @@ export default class MemberList extends React.Component {
|
|||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
|
|
|
@ -25,6 +25,7 @@ import Timer from '../../../utils/Timer';
|
|||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../utils/strings";
|
||||
|
||||
interface IProps {
|
||||
// the room this statusbar is representing.
|
||||
|
@ -207,7 +208,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
|||
usersTyping = usersTyping.concat(stoppedUsersOnTimer);
|
||||
// sort them so the typing members don't change order when
|
||||
// moved to delayedStopTypingTimers
|
||||
usersTyping.sort((a, b) => a.name.localeCompare(b.name));
|
||||
usersTyping.sort((a, b) => compare(a.name, b.name));
|
||||
|
||||
const typingString = WhoIsTyping.whoIsTypingString(
|
||||
usersTyping,
|
||||
|
|
|
@ -25,6 +25,7 @@ import {EventType} from "matrix-js-sdk/src/@types/event";
|
|||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
|
||||
const plEventsToLabels = {
|
||||
// These will be translated for us later.
|
||||
|
@ -312,7 +313,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
|
||||
const comparator = (a, b) => {
|
||||
const plDiff = userLevels[b.key] - userLevels[a.key];
|
||||
return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase());
|
||||
return plDiff !== 0 ? plDiff : compare(a.key.toLocaleLowerCase(), b.key.toLocaleLowerCase());
|
||||
};
|
||||
|
||||
privilegedUsers.sort(comparator);
|
||||
|
|
|
@ -35,9 +35,10 @@ import Field from '../../../elements/Field';
|
|||
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
import {Layout} from "../../../../../settings/Layout";
|
||||
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { Layout } from "../../../../../settings/Layout";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../../../utils/strings";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -295,7 +296,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
|
||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
.sort((a, b) => compare(a.name, b.name));
|
||||
const orderedThemes = [...builtInThemes, ...customThemes];
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
|
||||
|
|
|
@ -28,6 +28,7 @@ import WidgetUtils from "../utils/WidgetUtils";
|
|||
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import url from 'url';
|
||||
import { compare } from "../utils/strings";
|
||||
|
||||
const KIND_PREFERENCE = [
|
||||
// Ordered: first is most preferred, last is least preferred.
|
||||
|
@ -152,7 +153,7 @@ export class IntegrationManagers {
|
|||
|
||||
if (kind === Kind.Account) {
|
||||
// Order by state_keys (IDs)
|
||||
managers.sort((a, b) => a.id.localeCompare(b.id));
|
||||
managers.sort((a, b) => compare(a.id, b.id));
|
||||
}
|
||||
|
||||
ordered.push(...managers);
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { TagID } from "../../models";
|
||||
import { IAlgorithm } from "./IAlgorithm";
|
||||
import { compare } from "../../../../utils/strings";
|
||||
|
||||
/**
|
||||
* Sorts rooms according to the browser's determination of alphabetic.
|
||||
|
@ -24,7 +25,7 @@ import { IAlgorithm } from "./IAlgorithm";
|
|||
export class AlphabeticAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
return rooms.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
return compare(a.name, b.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
import { compare } from "../../utils/strings";
|
||||
|
||||
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
|
||||
|
||||
|
@ -240,7 +241,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||
|
||||
if (orderA === orderB) {
|
||||
// We just need a tiebreak
|
||||
return a.id.localeCompare(b.id);
|
||||
return compare(a.id, b.id);
|
||||
}
|
||||
|
||||
return orderA - orderB;
|
||||
|
|
|
@ -73,3 +73,14 @@ export function copyNode(ref: Element): boolean {
|
|||
selectText(ref);
|
||||
return document.execCommand('copy');
|
||||
}
|
||||
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
/**
|
||||
* Performant language-sensitive string comparison
|
||||
* @param a the first string to compare
|
||||
* @param b the second string to compare
|
||||
*/
|
||||
export function compare(a: string, b: string): number {
|
||||
return collator.compare(a, b);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import sdk from '../../../skinned-sdk';
|
|||
|
||||
import {Room, RoomMember, User} from 'matrix-js-sdk';
|
||||
|
||||
import { compare } from "../../../../src/utils/strings";
|
||||
|
||||
function generateRoomId() {
|
||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||
}
|
||||
|
@ -173,7 +175,7 @@ describe('MemberList', () => {
|
|||
if (!groupChange) {
|
||||
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
|
||||
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
|
||||
const nameCompare = nameB.localeCompare(nameA);
|
||||
const nameCompare = compare(nameB, nameA);
|
||||
console.log("Comparing name");
|
||||
expect(nameCompare).toBeGreaterThanOrEqual(0);
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue