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