Merge branch 'develop' into gsouquet/voice-messages-waveform-perf
This commit is contained in:
commit
6cb86057c5
46 changed files with 902 additions and 633 deletions
|
@ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from '../../../email';
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
|
|
|
@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
|||
render() {
|
||||
const cli = this.context;
|
||||
const room = this.props.room;
|
||||
const inRoomChannel = cli.crypto._inRoomVerificationRequests;
|
||||
const inRoomChannel = cli.crypto.inRoomVerificationRequests;
|
||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||
|
||||
return (<div>
|
||||
|
|
|
@ -17,37 +17,45 @@ limitations under the License.
|
|||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as Email from "../../../email";
|
||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
|
||||
import { abbreviateUrl } from "../../../utils/UrlUtils";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import { humanizeTime } from "../../../utils/humanize";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||
canEncryptToAllUsers,
|
||||
ensureDMExists,
|
||||
findDMForUser,
|
||||
privateShouldBeEncrypted,
|
||||
} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
import {
|
||||
IInviteResult,
|
||||
inviteMultipleToRoom,
|
||||
showAnyInviteErrors,
|
||||
showCommunityInviteDialog,
|
||||
} from "../../../RoomInvite";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {getAddressType} from "../../../UserAddress";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { getAddressType } from "../../../UserAddress";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { compare } from '../../../utils/strings';
|
||||
|
@ -74,10 +82,10 @@ export const KIND_CALL_TRANSFER = "call_transfer";
|
|||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
|
||||
// This is the interface that is expected by various components in this file. It is a bit
|
||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
||||
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// for 3PIDs/email addresses.
|
||||
abstract class Member {
|
||||
export abstract class Member {
|
||||
/**
|
||||
* The display name of this Member. For users this should be their profile's display
|
||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||
|
@ -102,7 +110,8 @@ class DirectoryMember extends Member {
|
|||
private readonly displayName: string;
|
||||
private readonly avatarUrl: string;
|
||||
|
||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||
// eslint-disable-next-line camelcase
|
||||
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this.displayName = userDirResult.display_name;
|
||||
|
@ -601,19 +610,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||
}
|
||||
|
||||
private shouldAbortAfterInviteError(result): boolean {
|
||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
||||
if (failedUsers.length > 0) {
|
||||
console.log("Failed to invite users: ", result);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
|
||||
csvUsers: failedUsers.join(", "),
|
||||
}),
|
||||
});
|
||||
return true; // abort
|
||||
}
|
||||
return false;
|
||||
private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
|
||||
this.setState({ busy: false });
|
||||
const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member]));
|
||||
return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
|
||||
}
|
||||
|
||||
private convertFilter(): Member[] {
|
||||
|
@ -731,7 +731,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
try {
|
||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||
if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||
if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { UserAddressType } from '../../../UserAddress.js';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { UserAddressType } from '../../../UserAddress';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.elements.AddressTile")
|
||||
export default class AddressTile extends React.Component {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import Flair from '../elements/Flair';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
|
|
@ -28,7 +28,7 @@ export default class TextualEvent extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
|
||||
if (text == null || text.length === 0) return null;
|
||||
return (
|
||||
<div className="mx_TextualEvent">{ text }</div>
|
||||
|
|
|
@ -503,7 +503,7 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
|
|||
return member.powerLevel < levelToSend;
|
||||
};
|
||||
|
||||
const getPowerLevels = room => room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||
const getPowerLevels = room => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||
|
||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
||||
|
|
|
@ -17,13 +17,23 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _td } from '../../../languageHandler';
|
||||
import classNames from "classnames";
|
||||
import E2EIcon from './E2EIcon';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import PresenceLabel from "./PresenceLabel";
|
||||
|
||||
export enum PowerStatus {
|
||||
Admin = "admin",
|
||||
Moderator = "moderator",
|
||||
}
|
||||
|
||||
const PowerLabel: Record<PowerStatus, string> = {
|
||||
[PowerStatus.Admin]: _td("Admin"),
|
||||
[PowerStatus.Moderator]: _td("Mod"),
|
||||
}
|
||||
|
||||
const PRESENCE_CLASS = {
|
||||
"offline": "mx_EntityTile_offline",
|
||||
|
@ -31,14 +41,14 @@ const PRESENCE_CLASS = {
|
|||
"unavailable": "mx_EntityTile_unavailable",
|
||||
};
|
||||
|
||||
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
|
||||
function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean): string {
|
||||
if (showPresence === false) {
|
||||
return 'mx_EntityTile_online_beenactive';
|
||||
}
|
||||
|
||||
// offline is split into two categories depending on whether we have
|
||||
// a last_active_ago for them.
|
||||
if (presenceState == 'offline') {
|
||||
if (presenceState === 'offline') {
|
||||
if (lastActiveAgo) {
|
||||
return PRESENCE_CLASS['offline'] + '_beenactive';
|
||||
} else {
|
||||
|
@ -51,29 +61,32 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
|
|||
}
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EntityTile")
|
||||
class EntityTile extends React.Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
avatarJsx: PropTypes.any, // <BaseAvatar />
|
||||
className: PropTypes.string,
|
||||
presenceState: PropTypes.string,
|
||||
presenceLastActiveAgo: PropTypes.number,
|
||||
presenceLastTs: PropTypes.number,
|
||||
presenceCurrentlyActive: PropTypes.bool,
|
||||
showInviteButton: PropTypes.bool,
|
||||
shouldComponentUpdate: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
suppressOnHover: PropTypes.bool,
|
||||
showPresence: PropTypes.bool,
|
||||
subtextLabel: PropTypes.string,
|
||||
e2eStatus: PropTypes.string,
|
||||
};
|
||||
interface IProps {
|
||||
name?: string;
|
||||
title?: string;
|
||||
avatarJsx?: JSX.Element; // <BaseAvatar />
|
||||
className?: string;
|
||||
presenceState?: string;
|
||||
presenceLastActiveAgo?: number;
|
||||
presenceLastTs?: number;
|
||||
presenceCurrentlyActive?: boolean;
|
||||
showInviteButton?: boolean;
|
||||
onClick?(): void;
|
||||
suppressOnHover?: boolean;
|
||||
showPresence?: boolean;
|
||||
subtextLabel?: string;
|
||||
e2eStatus?: string;
|
||||
powerStatus?: PowerStatus;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EntityTile")
|
||||
export default class EntityTile extends React.PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
shouldComponentUpdate: function(nextProps, nextState) { return true; },
|
||||
onClick: function() {},
|
||||
onClick: () => {},
|
||||
presenceState: "offline",
|
||||
presenceLastActiveAgo: 0,
|
||||
presenceLastTs: 0,
|
||||
|
@ -82,13 +95,12 @@ class EntityTile extends React.Component {
|
|||
showPresence: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
hover: false,
|
||||
};
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (this.state.hover !== nextState.hover) return true;
|
||||
return this.props.shouldComponentUpdate(nextProps, nextState);
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -110,7 +122,6 @@ class EntityTile extends React.Component {
|
|||
const activeAgo = this.props.presenceLastActiveAgo ?
|
||||
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
|
||||
|
||||
const PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
|
||||
let presenceLabel = null;
|
||||
if (this.props.showPresence) {
|
||||
presenceLabel = <PresenceLabel activeAgo={activeAgo}
|
||||
|
@ -155,10 +166,7 @@ class EntityTile extends React.Component {
|
|||
let powerLabel;
|
||||
const powerStatus = this.props.powerStatus;
|
||||
if (powerStatus) {
|
||||
const powerText = {
|
||||
[EntityTile.POWER_STATUS_MODERATOR]: _t("Mod"),
|
||||
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
|
||||
}[powerStatus];
|
||||
const powerText = PowerLabel[powerStatus];
|
||||
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
|
||||
}
|
||||
|
||||
|
@ -168,14 +176,12 @@ class EntityTile extends React.Component {
|
|||
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />;
|
||||
}
|
||||
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
const av = this.props.avatarJsx ||
|
||||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />;
|
||||
|
||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||
return (
|
||||
<div ref={(c) => this.container = c} >
|
||||
<div>
|
||||
<AccessibleButton
|
||||
className={classNames(mainClassNames)}
|
||||
title={this.props.title}
|
||||
|
@ -193,8 +199,3 @@ class EntityTile extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
EntityTile.POWER_STATUS_MODERATOR = "moderator";
|
||||
EntityTile.POWER_STATUS_ADMIN = "admin";
|
||||
|
||||
export default EntityTile;
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,17 +21,28 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
import rate_limited_func from "../../../ratelimitedfunc";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import rateLimitedFunction from "../../../ratelimitedfunc";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import TruncatedList from '../elements/TruncatedList';
|
||||
import Spinner from "../elements/Spinner";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import EntityTile from "./EntityTile";
|
||||
import MemberTile from "./MemberTile";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -40,41 +52,59 @@ const SHOW_MORE_INCREMENT = 100;
|
|||
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
||||
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
members: Array<RoomMember>;
|
||||
filteredJoinedMembers: Array<RoomMember>;
|
||||
filteredInvitedMembers: Array<RoomMember | MatrixEvent>;
|
||||
canInvite: boolean;
|
||||
truncateAtJoined: number;
|
||||
truncateAtInvited: number;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.MemberList")
|
||||
export default class MemberList extends React.Component {
|
||||
export default class MemberList extends React.Component<IProps, IState> {
|
||||
private showPresence = true;
|
||||
private mounted = false;
|
||||
private collator: Intl.Collator;
|
||||
private sortNames = new Map<RoomMember, string>(); // RoomMember -> sortName
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
// show an empty list
|
||||
this.state = this._getMembersState([]);
|
||||
this.state = this.getMembersState([]);
|
||||
} else {
|
||||
this.state = this._getMembersState(this.roomMembers());
|
||||
this.state = this.getMembersState(this.roomMembers());
|
||||
}
|
||||
|
||||
cli.on("Room", this.onRoom); // invites & joining after peek
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||
this._showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
}
|
||||
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
this._mounted = true;
|
||||
this.mounted = true;
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
cli.on("Room.myMembership", this.onMyMembership);
|
||||
} else {
|
||||
this._listenForMembersChanges();
|
||||
this.listenForMembersChanges();
|
||||
}
|
||||
}
|
||||
|
||||
_listenForMembersChanges() {
|
||||
private listenForMembersChanges(): void {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
|
@ -89,7 +119,7 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
this.mounted = false;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
|
@ -103,7 +133,7 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateList.cancelPendingCall();
|
||||
this.updateList.cancelPendingCall();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,7 +141,7 @@ export default class MemberList extends React.Component {
|
|||
* show a spinner and load the members if the user is joined,
|
||||
* or show the members available so far if the user is invited
|
||||
*/
|
||||
async _showMembersAccordingToMembershipWithLL() {
|
||||
private async showMembersAccordingToMembershipWithLL(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -122,31 +152,31 @@ export default class MemberList extends React.Component {
|
|||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
} catch (ex) {/* already logged in RoomView */}
|
||||
if (this._mounted) {
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
this._listenForMembersChanges();
|
||||
if (this.mounted) {
|
||||
this.setState(this.getMembersState(this.roomMembers()));
|
||||
this.listenForMembersChanges();
|
||||
}
|
||||
} else {
|
||||
// show the members we already have loaded
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
this.setState(this.getMembersState(this.roomMembers()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get canInvite() {
|
||||
private get canInvite(): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
return room && room.canInvite(cli.getUserId());
|
||||
}
|
||||
|
||||
_getMembersState(members) {
|
||||
// set the state after determining _showPresence to make sure it's
|
||||
// taken into account while rerendering
|
||||
private getMembersState(members: Array<RoomMember>): IState {
|
||||
// set the state after determining showPresence to make sure it's
|
||||
// taken into account while rendering
|
||||
return {
|
||||
loading: false,
|
||||
members: members,
|
||||
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
||||
filteredJoinedMembers: this.filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this.filterMembers(members, 'invite'),
|
||||
canInvite: this.canInvite,
|
||||
|
||||
// ideally we'd size this to the page height, but
|
||||
|
@ -157,72 +187,72 @@ export default class MemberList extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onUserPresenceChange = (event, user) => {
|
||||
private onUserPresenceChange = (event: MatrixEvent, user: User): void => {
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// ever attaching their own listener.
|
||||
const tile = this.refs[user.userId];
|
||||
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
||||
if (tile) {
|
||||
this._updateList(); // reorder the membership list
|
||||
this.updateList(); // reorder the membership list
|
||||
}
|
||||
};
|
||||
|
||||
onRoom = room => {
|
||||
private onRoom = (room: Room): void => {
|
||||
if (room.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
// We listen for room events because when we accept an invite
|
||||
// we need to wait till the room is fully populated with state
|
||||
// before refreshing the member list else we get a stale list.
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
};
|
||||
|
||||
onMyMembership = (room, membership, oldMembership) => {
|
||||
private onMyMembership = (room: Room, membership: string, oldMembership: string): void => {
|
||||
if (room.roomId === this.props.roomId && membership === "join") {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
}
|
||||
};
|
||||
|
||||
onRoomStateMember = (ev, state, member) => {
|
||||
private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
};
|
||||
|
||||
onRoomMemberName = (ev, member) => {
|
||||
private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
};
|
||||
|
||||
onRoomStateEvent = (event, state) => {
|
||||
private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => {
|
||||
if (event.getRoomId() === this.props.roomId &&
|
||||
event.getType() === "m.room.third_party_invite") {
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
}
|
||||
|
||||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
||||
};
|
||||
|
||||
_updateList = rate_limited_func(() => {
|
||||
this._updateListNow();
|
||||
private updateList = rateLimitedFunction(() => {
|
||||
this.updateListNow();
|
||||
}, 500);
|
||||
|
||||
_updateListNow() {
|
||||
// console.log("Updating memberlist");
|
||||
const newState = {
|
||||
private updateListNow(): void {
|
||||
const members = this.roomMembers()
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
members: this.roomMembers(),
|
||||
};
|
||||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
|
||||
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
|
||||
this.setState(newState);
|
||||
members: members,
|
||||
filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery),
|
||||
filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery),
|
||||
});
|
||||
}
|
||||
|
||||
getMembersWithUser() {
|
||||
private getMembersWithUser(): Array<RoomMember> {
|
||||
if (!this.props.roomId) return [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
|
@ -230,15 +260,18 @@ export default class MemberList extends React.Component {
|
|||
|
||||
const allMembers = Object.values(room.currentState.members);
|
||||
|
||||
allMembers.forEach(function(member) {
|
||||
allMembers.forEach((member) => {
|
||||
// work around a race where you might have a room member object
|
||||
// before the user object exists. This may or may not cause
|
||||
// before the user object exists. This may or may not cause
|
||||
// https://github.com/vector-im/vector-web/issues/186
|
||||
if (member.user === null) {
|
||||
if (!member.user) {
|
||||
member.user = cli.getUser(member.userId);
|
||||
}
|
||||
|
||||
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
|
||||
this.sortNames.set(
|
||||
member,
|
||||
(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
|
||||
|
@ -247,7 +280,7 @@ export default class MemberList extends React.Component {
|
|||
return allMembers;
|
||||
}
|
||||
|
||||
roomMembers() {
|
||||
private roomMembers(): Array<RoomMember> {
|
||||
const allMembers = this.getMembersWithUser();
|
||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||
return (
|
||||
|
@ -255,23 +288,21 @@ export default class MemberList extends React.Component {
|
|||
);
|
||||
});
|
||||
const language = SettingsStore.getValue("language");
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false });
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
}
|
||||
|
||||
_createOverflowTileJoined = (overflowCount, totalCount) => {
|
||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
|
||||
private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||
return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList);
|
||||
};
|
||||
|
||||
_createOverflowTileInvited = (overflowCount, totalCount) => {
|
||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
|
||||
private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||
return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList);
|
||||
};
|
||||
|
||||
_createOverflowTile = (overflowCount, totalCount, onClick) => {
|
||||
private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element=> {
|
||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
|
@ -281,31 +312,48 @@ export default class MemberList extends React.Component {
|
|||
);
|
||||
};
|
||||
|
||||
_showMoreJoinedMemberList = () => {
|
||||
private showMoreJoinedMemberList = (): void => {
|
||||
this.setState({
|
||||
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
||||
});
|
||||
};
|
||||
|
||||
_showMoreInvitedMemberList = () => {
|
||||
private showMoreInvitedMemberList = (): void => {
|
||||
this.setState({
|
||||
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
||||
});
|
||||
};
|
||||
|
||||
memberString(member) {
|
||||
/**
|
||||
* SHOULD ONLY BE USED BY TESTS
|
||||
*/
|
||||
public memberString(member: RoomMember): string {
|
||||
if (!member) {
|
||||
return "(null)";
|
||||
} else {
|
||||
const u = member.user;
|
||||
return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "<null>") + ", " + (u ? u.getLastActiveTs() : "<null>") + ", " + (u ? u.currentlyActive : "<null>") + ", " + (u ? u.presence : "<null>") + ")";
|
||||
return (
|
||||
"(" +
|
||||
member.name +
|
||||
", " +
|
||||
member.powerLevel +
|
||||
", " +
|
||||
(u ? u.lastActiveAgo : "<null>") +
|
||||
", " +
|
||||
(u ? u.getLastActiveTs() : "<null>") +
|
||||
", " +
|
||||
(u ? u.currentlyActive : "<null>") +
|
||||
", " +
|
||||
(u ? u.presence : "<null>") +
|
||||
")"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// returns negative if a comes before b,
|
||||
// returns 0 if a and b are equivalent in ordering
|
||||
// returns positive if a comes after b.
|
||||
memberSort = (memberA, memberB) => {
|
||||
private memberSort = (memberA: RoomMember, memberB: RoomMember): number => {
|
||||
// order by presence, with "active now" first.
|
||||
// ...and then by power level
|
||||
// ...and then by last active
|
||||
|
@ -325,7 +373,7 @@ export default class MemberList extends React.Component {
|
|||
if (!userA && userB) return 1;
|
||||
|
||||
// First by presence
|
||||
if (this._showPresence) {
|
||||
if (this.showPresence) {
|
||||
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
||||
const presenceIndex = p => {
|
||||
const order = ['active', 'online', 'offline'];
|
||||
|
@ -349,31 +397,31 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
// Third by last active
|
||||
if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
||||
if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
||||
// console.log("Comparing on last active timestamp - returning");
|
||||
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
||||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
return this.collator.compare(memberA.sortName, memberB.sortName);
|
||||
return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB));
|
||||
};
|
||||
|
||||
onSearchQueryChanged = searchQuery => {
|
||||
private onSearchQueryChanged = (searchQuery: string): void => {
|
||||
this.setState({
|
||||
searchQuery,
|
||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
|
||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
|
||||
filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
|
||||
filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery),
|
||||
});
|
||||
};
|
||||
|
||||
_onPending3pidInviteClick = inviteEvent => {
|
||||
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
|
||||
dis.dispatch({
|
||||
action: 'view_3pid_invite',
|
||||
event: inviteEvent,
|
||||
});
|
||||
};
|
||||
|
||||
_filterMembers(members, membership, query) {
|
||||
private filterMembers(members: Array<RoomMember>, membership: string, query?: string): Array<RoomMember> {
|
||||
return members.filter((m) => {
|
||||
if (query) {
|
||||
query = query.toLowerCase();
|
||||
|
@ -389,7 +437,7 @@ export default class MemberList extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_getPending3PidInvites() {
|
||||
private getPending3PidInvites(): Array<MatrixEvent> {
|
||||
// include 3pid invites (m.room.third_party_invite) state events.
|
||||
// The HS may have already converted these into m.room.member invites so
|
||||
// we shouldn't add them if the 3pid invite state key (token) is in the
|
||||
|
@ -409,42 +457,40 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_makeMemberTiles(members) {
|
||||
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
|
||||
private makeMemberTiles(members: Array<RoomMember | MatrixEvent>) {
|
||||
return members.map((m) => {
|
||||
if (m.userId) {
|
||||
if (m instanceof RoomMember) {
|
||||
// Is a Matrix invite
|
||||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />;
|
||||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
|
||||
} 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)} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
|
||||
|
||||
_getChildCountJoined = () => this.state.filteredJoinedMembers.length;
|
||||
|
||||
_getChildrenInvited = (start, end) => {
|
||||
let targets = this.state.filteredInvitedMembers;
|
||||
if (end > this.state.filteredInvitedMembers.length) {
|
||||
targets = targets.concat(this._getPending3PidInvites());
|
||||
}
|
||||
|
||||
return this._makeMemberTiles(targets.slice(start, end));
|
||||
private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => {
|
||||
return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end))
|
||||
};
|
||||
|
||||
_getChildCountInvited = () => {
|
||||
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length;
|
||||
private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length;
|
||||
|
||||
private getChildrenInvited = (start: number, end: number): Array<JSX.Element> => {
|
||||
let targets = this.state.filteredInvitedMembers;
|
||||
if (end > this.state.filteredInvitedMembers.length) {
|
||||
targets = targets.concat(this.getPending3PidInvites());
|
||||
}
|
||||
|
||||
return this.makeMemberTiles(targets.slice(start, end));
|
||||
};
|
||||
|
||||
private getChildCountInvited = (): number => {
|
||||
return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
onClose={this.props.onClose}
|
||||
|
@ -454,9 +500,6 @@ export default class MemberList extends React.Component {
|
|||
</BaseCard>;
|
||||
}
|
||||
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
let inviteButton;
|
||||
|
@ -470,22 +513,30 @@ export default class MemberList extends React.Component {
|
|||
inviteButtonText = _t("Invite to this space");
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
inviteButton =
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!this.state.canInvite}>
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
className="mx_MemberList_invite"
|
||||
onClick={this.onInviteButtonClick}
|
||||
disabled={!this.state.canInvite}
|
||||
>
|
||||
<span>{ inviteButtonText }</span>
|
||||
</AccessibleButton>;
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let invitedHeader;
|
||||
let invitedSection;
|
||||
if (this._getChildCountInvited() > 0) {
|
||||
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}
|
||||
/>;
|
||||
invitedSection = (
|
||||
<TruncatedList
|
||||
className="mx_MemberList_section mx_MemberList_invited"
|
||||
truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this.createOverflowTileInvited}
|
||||
getChildren={this.getChildrenInvited}
|
||||
getChildCount={this.getChildCountInvited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const footer = (
|
||||
|
@ -517,17 +568,19 @@ export default class MemberList extends React.Component {
|
|||
previousPhase={previousPhase}
|
||||
>
|
||||
<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} />
|
||||
<TruncatedList
|
||||
className="mx_MemberList_section mx_MemberList_joined"
|
||||
truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this.createOverflowTileJoined}
|
||||
getChildren={this.getChildrenJoined}
|
||||
getChildCount={this.getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
</BaseCard>;
|
||||
}
|
||||
|
||||
onInviteButtonClick = () => {
|
||||
onInviteButtonClick = (): void => {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
|
@ -17,20 +17,33 @@ limitations under the License.
|
|||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from "../../../index";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||
import EntityTile, { PowerStatus } from "./EntityTile";
|
||||
import MemberAvatar from "./../avatars/MemberAvatar";
|
||||
|
||||
interface IProps {
|
||||
member: RoomMember;
|
||||
showPresence?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
statusMessage: string;
|
||||
isRoomEncrypted: boolean;
|
||||
e2eStatus: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.MemberTile")
|
||||
export default class MemberTile extends React.Component {
|
||||
static propTypes = {
|
||||
member: PropTypes.any.isRequired, // RoomMember
|
||||
showPresence: PropTypes.bool,
|
||||
};
|
||||
export default class MemberTile extends React.Component<IProps, IState> {
|
||||
private userLastModifiedTime: number;
|
||||
private memberLastModifiedTime: number;
|
||||
|
||||
static defaultProps = {
|
||||
showPresence: true,
|
||||
|
@ -52,7 +65,7 @@ export default class MemberTile extends React.Component {
|
|||
if (SettingsStore.getValue("feature_custom_status")) {
|
||||
const { user } = this.props.member;
|
||||
if (user) {
|
||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
||||
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,7 +93,7 @@ export default class MemberTile extends React.Component {
|
|||
if (user) {
|
||||
user.removeListener(
|
||||
"User._unstable_statusMessage",
|
||||
this._onStatusMessageCommitted,
|
||||
this.onStatusMessageCommitted,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -91,8 +104,8 @@ export default class MemberTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onRoomStateEvents = ev => {
|
||||
if (ev.getType() !== "m.room.encryption") return;
|
||||
private onRoomStateEvents = (ev: MatrixEvent): void => {
|
||||
if (ev.getType() !== EventType.RoomEncryption) return;
|
||||
const { roomId } = this.props.member;
|
||||
if (ev.getRoomId() !== roomId) return;
|
||||
|
||||
|
@ -105,17 +118,17 @@ export default class MemberTile extends React.Component {
|
|||
this.updateE2EStatus();
|
||||
};
|
||||
|
||||
onUserTrustStatusChanged = (userId, trustStatus) => {
|
||||
private onUserTrustStatusChanged = (userId: string, trustStatus: string): void => {
|
||||
if (userId !== this.props.member.userId) return;
|
||||
this.updateE2EStatus();
|
||||
};
|
||||
|
||||
onDeviceVerificationChanged = (userId, deviceId, deviceInfo) => {
|
||||
private onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => {
|
||||
if (userId !== this.props.member.userId) return;
|
||||
this.updateE2EStatus();
|
||||
};
|
||||
|
||||
async updateE2EStatus() {
|
||||
private async updateE2EStatus(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const { userId } = this.props.member;
|
||||
const isMe = userId === cli.getUserId();
|
||||
|
@ -143,32 +156,32 @@ export default class MemberTile extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
getStatusMessage() {
|
||||
private getStatusMessage(): string {
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
return "";
|
||||
}
|
||||
return user._unstable_statusMessage;
|
||||
return user.unstable_statusMessage;
|
||||
}
|
||||
|
||||
_onStatusMessageCommitted = () => {
|
||||
private onStatusMessageCommitted = (): void => {
|
||||
// The `User` object has observed a status message change.
|
||||
this.setState({
|
||||
statusMessage: this.getStatusMessage(),
|
||||
});
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean {
|
||||
if (
|
||||
this.member_last_modified_time === undefined ||
|
||||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
|
||||
this.memberLastModifiedTime === undefined ||
|
||||
this.memberLastModifiedTime < nextProps.member.getLastModifiedTime()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
nextProps.member.user &&
|
||||
(this.user_last_modified_time === undefined ||
|
||||
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime())
|
||||
(this.userLastModifiedTime === undefined ||
|
||||
this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@ -181,18 +194,18 @@ export default class MemberTile extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
onClick = e => {
|
||||
private onClick = (): void => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: this.props.member,
|
||||
});
|
||||
};
|
||||
|
||||
_getDisplayName() {
|
||||
private getDisplayName(): string {
|
||||
return this.props.member.name;
|
||||
}
|
||||
|
||||
getPowerLabel() {
|
||||
private getPowerLabel(): string {
|
||||
return _t("%(userName)s (power %(powerLevelNumber)s)", {
|
||||
userName: this.props.member.userId,
|
||||
powerLevelNumber: this.props.member.powerLevel,
|
||||
|
@ -200,11 +213,8 @@ export default class MemberTile extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||
|
||||
const member = this.props.member;
|
||||
const name = this._getDisplayName();
|
||||
const name = this.getDisplayName();
|
||||
const presenceState = member.user ? member.user.presence : null;
|
||||
|
||||
let statusMessage = null;
|
||||
|
@ -217,13 +227,13 @@ export default class MemberTile extends React.Component {
|
|||
);
|
||||
|
||||
if (member.user) {
|
||||
this.user_last_modified_time = member.user.getLastModifiedTime();
|
||||
this.userLastModifiedTime = member.user.getLastModifiedTime();
|
||||
}
|
||||
this.member_last_modified_time = member.getLastModifiedTime();
|
||||
this.memberLastModifiedTime = member.getLastModifiedTime();
|
||||
|
||||
const powerStatusMap = new Map([
|
||||
[100, EntityTile.POWER_STATUS_ADMIN],
|
||||
[50, EntityTile.POWER_STATUS_MODERATOR],
|
||||
[100, PowerStatus.Admin],
|
||||
[50, PowerStatus.Moderator],
|
||||
]);
|
||||
|
||||
// Find the nearest power level with a badge
|
|
@ -15,26 +15,23 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// number of milliseconds ago this user was last active.
|
||||
// zero = unknown
|
||||
activeAgo?: number;
|
||||
// if true, activeAgo is an approximation and "Now" should
|
||||
// be shown instead
|
||||
currentlyActive?: boolean;
|
||||
// offline, online, etc
|
||||
presenceState?: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.PresenceLabel")
|
||||
export default class PresenceLabel extends React.Component {
|
||||
static propTypes = {
|
||||
// number of milliseconds ago this user was last active.
|
||||
// zero = unknown
|
||||
activeAgo: PropTypes.number,
|
||||
|
||||
// if true, activeAgo is an approximation and "Now" should
|
||||
// be shown instead
|
||||
currentlyActive: PropTypes.bool,
|
||||
|
||||
// offline, online, etc
|
||||
presenceState: PropTypes.string,
|
||||
};
|
||||
|
||||
export default class PresenceLabel extends React.Component<IProps> {
|
||||
static defaultProps = {
|
||||
activeAgo: -1,
|
||||
presenceState: null,
|
||||
|
@ -42,29 +39,29 @@ export default class PresenceLabel extends React.Component {
|
|||
|
||||
// Return duration as a string using appropriate time units
|
||||
// XXX: This would be better handled using a culture-aware library, but we don't use one yet.
|
||||
getDuration(time) {
|
||||
private getDuration(time: number): string {
|
||||
if (!time) return;
|
||||
const t = parseInt(time / 1000);
|
||||
const t = Math.round(time / 1000);
|
||||
const s = t % 60;
|
||||
const m = parseInt(t / 60) % 60;
|
||||
const h = parseInt(t / (60 * 60)) % 24;
|
||||
const d = parseInt(t / (60 * 60 * 24));
|
||||
const m = Math.round(t / 60) % 60;
|
||||
const h = Math.round(t / (60 * 60)) % 24;
|
||||
const d = Math.round(t / (60 * 60 * 24));
|
||||
if (t < 60) {
|
||||
if (t < 0) {
|
||||
return _t("%(duration)ss", {duration: 0});
|
||||
return _t("%(duration)ss", { duration: 0 });
|
||||
}
|
||||
return _t("%(duration)ss", {duration: s});
|
||||
return _t("%(duration)ss", { duration: s });
|
||||
}
|
||||
if (t < 60 * 60) {
|
||||
return _t("%(duration)sm", {duration: m});
|
||||
return _t("%(duration)sm", { duration: m });
|
||||
}
|
||||
if (t < 24 * 60 * 60) {
|
||||
return _t("%(duration)sh", {duration: h});
|
||||
return _t("%(duration)sh", { duration: h });
|
||||
}
|
||||
return _t("%(duration)sd", {duration: d});
|
||||
return _t("%(duration)sd", { duration: d });
|
||||
}
|
||||
|
||||
getPrettyPresence(presence, activeAgo, currentlyActive) {
|
||||
private getPrettyPresence(presence: string, activeAgo: number, currentlyActive: boolean): string {
|
||||
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
|
||||
const duration = this.getDuration(activeAgo);
|
||||
if (presence === "online") return _t("Online for %(duration)s", { duration: duration });
|
|
@ -45,7 +45,6 @@ import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
|||
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import SpaceStore, {ISuggestedRoom, SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
|
||||
import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
@ -103,38 +102,6 @@ interface ITagAestheticsMap {
|
|||
[tagId: TagID]: ITagAesthetics;
|
||||
}
|
||||
|
||||
// If we have no dialer support, we just show the create chat dialog
|
||||
const dmOnAddRoom = (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
|
||||
};
|
||||
|
||||
// If we have dialer support, show a context menu so the user can pick between
|
||||
// the dialer and the create chat dialog
|
||||
const dmAddRoomContextMenu = (onFinished: () => void) => {
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start a Conversation")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.dispatch({action: "view_create_chat"});
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Open dial pad")}
|
||||
iconClassName="mx_RoomList_iconDialpad"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.fire(Action.OpenDialPad);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
};
|
||||
|
||||
const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||
[DefaultTagID.Invite]: {
|
||||
sectionLabel: _td("Invites"),
|
||||
|
@ -151,8 +118,9 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Start chat"),
|
||||
// Either onAddRoom or addRoomContextMenu are set depending on whether we
|
||||
// have dialer support.
|
||||
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
|
||||
},
|
||||
},
|
||||
[DefaultTagID.Untagged]: {
|
||||
sectionLabel: _td("Rooms"),
|
||||
|
@ -271,7 +239,6 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
|||
export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
private tagAesthetics: ITagAestheticsMap;
|
||||
private roomStoreToken: fbEmitter.EventSubscription;
|
||||
|
||||
constructor(props: IProps) {
|
||||
|
@ -282,10 +249,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
||||
suggestedRooms: SpaceStore.instance.suggestedRooms,
|
||||
};
|
||||
|
||||
// shallow-copy from the template as we need to make modifications to it
|
||||
this.tagAesthetics = objectShallowClone(TAG_AESTHETICS);
|
||||
this.updateDmAddRoomAction();
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
|
@ -311,17 +274,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private updateDmAddRoomAction() {
|
||||
const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
|
||||
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||
dmTagAesthetics.addRoomContextMenu = dmAddRoomContextMenu;
|
||||
} else {
|
||||
dmTagAesthetics.onAddRoom = dmOnAddRoom;
|
||||
}
|
||||
|
||||
this.tagAesthetics[DefaultTagID.DM] = dmTagAesthetics;
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.ViewRoomDelta) {
|
||||
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
|
||||
|
@ -335,7 +287,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
} else if (payload.action === Action.PstnSupportUpdated) {
|
||||
this.updateDmAddRoomAction();
|
||||
this.updateLists();
|
||||
}
|
||||
};
|
||||
|
@ -524,7 +475,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
|
||||
? customTagAesthetics(orderedTagId)
|
||||
: this.tagAesthetics[orderedTagId];
|
||||
: TAG_AESTHETICS[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
// The cost of mounting/unmounting this component offsets the cost
|
||||
|
|
|
@ -114,12 +114,13 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
await this.state.recorder.stop();
|
||||
const mxc = await this.state.recorder.upload();
|
||||
const upload = await this.state.recorder.upload(this.props.room.roomId);
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
"body": "Voice message",
|
||||
//"msgtype": "org.matrix.msc2516.voice",
|
||||
"msgtype": MsgType.Audio,
|
||||
"url": mxc,
|
||||
"url": upload.mxc,
|
||||
"file": upload.encrypted,
|
||||
"info": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
mimetype: this.state.recorder.contentType,
|
||||
|
@ -130,7 +131,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
// https://github.com/matrix-org/matrix-doc/pull/3245
|
||||
"org.matrix.msc1767.text": "Voice message",
|
||||
"org.matrix.msc1767.file": {
|
||||
url: mxc,
|
||||
url: upload.mxc,
|
||||
file: upload.encrypted,
|
||||
name: "Voice message.ogg",
|
||||
mimetype: this.state.recorder.contentType,
|
||||
size: this.state.recorder.contentLength,
|
||||
|
|
|
@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
async _getUpdatedStatus() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const pkCache = cli.getCrossSigningCacheCallbacks();
|
||||
const crossSigning = cli.crypto._crossSigningInfo;
|
||||
const secretStorage = cli.crypto._secretStorage;
|
||||
const crossSigning = cli.crypto.crossSigningInfo;
|
||||
const secretStorage = cli.crypto.secretStorage;
|
||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||
|
|
|
@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
|
||||
async _getUpdatedDiagnostics() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secretStorage = cli.crypto._secretStorage;
|
||||
const secretStorage = cli.crypto.secretStorage;
|
||||
|
||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
||||
|
|
|
@ -33,6 +33,9 @@ export enum JoinRule {
|
|||
Public = "public",
|
||||
Knock = "knock",
|
||||
Invite = "invite",
|
||||
/**
|
||||
* @deprecated Reserved. Should not be used.
|
||||
*/
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
|
|
|
@ -129,7 +129,9 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
events_default: 100,
|
||||
...Visibility.Public ? { invite: 0 } : {},
|
||||
},
|
||||
room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
|
||||
room_alias_name: visibility === Visibility.Public && alias
|
||||
? alias.substr(1, alias.indexOf(":") - 1)
|
||||
: undefined,
|
||||
topic,
|
||||
},
|
||||
spinner: false,
|
||||
|
|
|
@ -62,9 +62,9 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
|
|||
const userId = cli.getUserId();
|
||||
|
||||
const [visibility, setVisibility] = useLocalEcho<SpaceVisibility>(
|
||||
() => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
|
||||
() => space.getJoinRule() === JoinRule.Invite ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
|
||||
visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, {
|
||||
join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private,
|
||||
join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Invite,
|
||||
}, ""),
|
||||
() => setError(_t("Failed to update the visibility of this space")),
|
||||
);
|
||||
|
|
|
@ -50,7 +50,7 @@ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
|
|||
{detailContent}
|
||||
</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
{onReject && rejectLabel && <AccessibleButton kind="danger" onClick={onReject}>
|
||||
{onReject && rejectLabel && <AccessibleButton kind="danger_outline" onClick={onReject}>
|
||||
{ rejectLabel }
|
||||
</AccessibleButton> }
|
||||
<AccessibleButton onClick={onAccept} kind="primary">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue