Merge pull request #6251 from SimonBrandner/ts/entity-and-member-tile

pull/21833/head
Michael Telatynski 2021-06-24 17:05:16 +01:00 committed by GitHub
commit 8f6d31b73c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 107 deletions

View File

@ -17,13 +17,23 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler'; import { _td } from '../../../languageHandler';
import classNames from "classnames"; import classNames from "classnames";
import E2EIcon from './E2EIcon'; 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 = { const PRESENCE_CLASS = {
"offline": "mx_EntityTile_offline", "offline": "mx_EntityTile_offline",
@ -31,14 +41,14 @@ const PRESENCE_CLASS = {
"unavailable": "mx_EntityTile_unavailable", "unavailable": "mx_EntityTile_unavailable",
}; };
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean): string {
if (showPresence === false) { if (showPresence === false) {
return 'mx_EntityTile_online_beenactive'; return 'mx_EntityTile_online_beenactive';
} }
// offline is split into two categories depending on whether we have // offline is split into two categories depending on whether we have
// a last_active_ago for them. // a last_active_ago for them.
if (presenceState == 'offline') { if (presenceState === 'offline') {
if (lastActiveAgo) { if (lastActiveAgo) {
return PRESENCE_CLASS['offline'] + '_beenactive'; return PRESENCE_CLASS['offline'] + '_beenactive';
} else { } else {
@ -51,29 +61,32 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
} }
} }
@replaceableComponent("views.rooms.EntityTile") interface IProps {
class EntityTile extends React.Component { name?: string;
static propTypes = { title?: string;
name: PropTypes.string, avatarJsx?: JSX.Element; // <BaseAvatar />
title: PropTypes.string, className?: string;
avatarJsx: PropTypes.any, // <BaseAvatar /> presenceState?: string;
className: PropTypes.string, presenceLastActiveAgo?: number;
presenceState: PropTypes.string, presenceLastTs?: number;
presenceLastActiveAgo: PropTypes.number, presenceCurrentlyActive?: boolean;
presenceLastTs: PropTypes.number, showInviteButton?: boolean;
presenceCurrentlyActive: PropTypes.bool, onClick?(): void;
showInviteButton: PropTypes.bool, suppressOnHover?: boolean;
shouldComponentUpdate: PropTypes.func, showPresence?: boolean;
onClick: PropTypes.func, subtextLabel?: string;
suppressOnHover: PropTypes.bool, e2eStatus?: string;
showPresence: PropTypes.bool, powerStatus?: PowerStatus;
subtextLabel: PropTypes.string, }
e2eStatus: PropTypes.string,
};
interface IState {
hover: boolean;
}
@replaceableComponent("views.rooms.EntityTile")
export default class EntityTile extends React.PureComponent<IProps, IState> {
static defaultProps = { static defaultProps = {
shouldComponentUpdate: function(nextProps, nextState) { return true; }, onClick: () => {},
onClick: function() {},
presenceState: "offline", presenceState: "offline",
presenceLastActiveAgo: 0, presenceLastActiveAgo: 0,
presenceLastTs: 0, presenceLastTs: 0,
@ -82,13 +95,12 @@ class EntityTile extends React.Component {
showPresence: true, showPresence: true,
}; };
state = { constructor(props: IProps) {
hover: false, super(props);
};
shouldComponentUpdate(nextProps, nextState) { this.state = {
if (this.state.hover !== nextState.hover) return true; hover: false,
return this.props.shouldComponentUpdate(nextProps, nextState); };
} }
render() { render() {
@ -110,7 +122,6 @@ class EntityTile extends React.Component {
const activeAgo = this.props.presenceLastActiveAgo ? const activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1; (Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
const PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
let presenceLabel = null; let presenceLabel = null;
if (this.props.showPresence) { if (this.props.showPresence) {
presenceLabel = <PresenceLabel activeAgo={activeAgo} presenceLabel = <PresenceLabel activeAgo={activeAgo}
@ -155,10 +166,7 @@ class EntityTile extends React.Component {
let powerLabel; let powerLabel;
const powerStatus = this.props.powerStatus; const powerStatus = this.props.powerStatus;
if (powerStatus) { if (powerStatus) {
const powerText = { const powerText = PowerLabel[powerStatus];
[EntityTile.POWER_STATUS_MODERATOR]: _t("Mod"),
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
}[powerStatus];
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>; 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} />; e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />;
} }
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const av = this.props.avatarJsx || const av = this.props.avatarJsx ||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />; <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. // The wrapping div is required to make the magic mouse listener work, for some reason.
return ( return (
<div ref={(c) => this.container = c} > <div>
<AccessibleButton <AccessibleButton
className={classNames(mainClassNames)} className={classNames(mainClassNames)}
title={this.props.title} 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;

View File

@ -17,20 +17,33 @@ limitations under the License.
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from "../../../index";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import {Action} from "../../../dispatcher/actions"; 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") @replaceableComponent("views.rooms.MemberTile")
export default class MemberTile extends React.Component { export default class MemberTile extends React.Component<IProps, IState> {
static propTypes = { private userLastModifiedTime: number;
member: PropTypes.any.isRequired, // RoomMember private memberLastModifiedTime: number;
showPresence: PropTypes.bool,
};
static defaultProps = { static defaultProps = {
showPresence: true, showPresence: true,
@ -52,7 +65,7 @@ export default class MemberTile extends React.Component {
if (SettingsStore.getValue("feature_custom_status")) { if (SettingsStore.getValue("feature_custom_status")) {
const { user } = this.props.member; const { user } = this.props.member;
if (user) { 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) { if (user) {
user.removeListener( user.removeListener(
"User._unstable_statusMessage", "User._unstable_statusMessage",
this._onStatusMessageCommitted, this.onStatusMessageCommitted,
); );
} }
@ -91,8 +104,8 @@ export default class MemberTile extends React.Component {
} }
} }
onRoomStateEvents = ev => { private onRoomStateEvents = (ev: MatrixEvent): void => {
if (ev.getType() !== "m.room.encryption") return; if (ev.getType() !== EventType.RoomEncryption) return;
const { roomId } = this.props.member; const { roomId } = this.props.member;
if (ev.getRoomId() !== roomId) return; if (ev.getRoomId() !== roomId) return;
@ -105,17 +118,17 @@ export default class MemberTile extends React.Component {
this.updateE2EStatus(); this.updateE2EStatus();
}; };
onUserTrustStatusChanged = (userId, trustStatus) => { private onUserTrustStatusChanged = (userId: string, trustStatus: string): void => {
if (userId !== this.props.member.userId) return; if (userId !== this.props.member.userId) return;
this.updateE2EStatus(); this.updateE2EStatus();
}; };
onDeviceVerificationChanged = (userId, deviceId, deviceInfo) => { private onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => {
if (userId !== this.props.member.userId) return; if (userId !== this.props.member.userId) return;
this.updateE2EStatus(); this.updateE2EStatus();
}; };
async updateE2EStatus() { private async updateE2EStatus(): Promise<void> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const { userId } = this.props.member; const { userId } = this.props.member;
const isMe = userId === cli.getUserId(); 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; const { user } = this.props.member;
if (!user) { if (!user) {
return ""; return "";
} }
return user._unstable_statusMessage; return user.unstable_statusMessage;
} }
_onStatusMessageCommitted = () => { private onStatusMessageCommitted = (): void => {
// The `User` object has observed a status message change. // The `User` object has observed a status message change.
this.setState({ this.setState({
statusMessage: this.getStatusMessage(), statusMessage: this.getStatusMessage(),
}); });
}; };
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean {
if ( if (
this.member_last_modified_time === undefined || this.memberLastModifiedTime === undefined ||
this.member_last_modified_time < nextProps.member.getLastModifiedTime() this.memberLastModifiedTime < nextProps.member.getLastModifiedTime()
) { ) {
return true; return true;
} }
if ( if (
nextProps.member.user && nextProps.member.user &&
(this.user_last_modified_time === undefined || (this.userLastModifiedTime === undefined ||
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime()) this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime())
) { ) {
return true; return true;
} }
@ -181,18 +194,18 @@ export default class MemberTile extends React.Component {
return false; return false;
} }
onClick = e => { private onClick = (): void => {
dis.dispatch({ dis.dispatch({
action: Action.ViewUser, action: Action.ViewUser,
member: this.props.member, member: this.props.member,
}); });
}; };
_getDisplayName() { private getDisplayName(): string {
return this.props.member.name; return this.props.member.name;
} }
getPowerLabel() { private getPowerLabel(): string {
return _t("%(userName)s (power %(powerLevelNumber)s)", { return _t("%(userName)s (power %(powerLevelNumber)s)", {
userName: this.props.member.userId, userName: this.props.member.userId,
powerLevelNumber: this.props.member.powerLevel, powerLevelNumber: this.props.member.powerLevel,
@ -200,11 +213,8 @@ export default class MemberTile extends React.Component {
} }
render() { render() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const EntityTile = sdk.getComponent('rooms.EntityTile');
const member = this.props.member; const member = this.props.member;
const name = this._getDisplayName(); const name = this.getDisplayName();
const presenceState = member.user ? member.user.presence : null; const presenceState = member.user ? member.user.presence : null;
let statusMessage = null; let statusMessage = null;
@ -217,13 +227,13 @@ export default class MemberTile extends React.Component {
); );
if (member.user) { 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([ const powerStatusMap = new Map([
[100, EntityTile.POWER_STATUS_ADMIN], [100, PowerStatus.Admin],
[50, EntityTile.POWER_STATUS_MODERATOR], [50, PowerStatus.Moderator],
]); ]);
// Find the nearest power level with a badge // Find the nearest power level with a badge

View File

@ -15,26 +15,23 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; 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") @replaceableComponent("views.rooms.PresenceLabel")
export default class PresenceLabel extends React.Component { export default class PresenceLabel extends React.Component<IProps> {
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,
};
static defaultProps = { static defaultProps = {
activeAgo: -1, activeAgo: -1,
presenceState: null, presenceState: null,
@ -42,29 +39,29 @@ export default class PresenceLabel extends React.Component {
// Return duration as a string using appropriate time units // 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. // 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; if (!time) return;
const t = parseInt(time / 1000); const t = time / 1000;
const s = t % 60; const s = t % 60;
const m = parseInt(t / 60) % 60; const m = t / 60 % 60;
const h = parseInt(t / (60 * 60)) % 24; const h = t / (60 * 60) % 24;
const d = parseInt(t / (60 * 60 * 24)); const d = t / (60 * 60 * 24);
if (t < 60) { if (t < 60) {
if (t < 0) { 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) { if (t < 60 * 60) {
return _t("%(duration)sm", {duration: m}); return _t("%(duration)sm", { duration: m });
} }
if (t < 24 * 60 * 60) { 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) { if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
const duration = this.getDuration(activeAgo); const duration = this.getDuration(activeAgo);
if (presence === "online") return _t("Online for %(duration)s", { duration: duration }); if (presence === "online") return _t("Online for %(duration)s", { duration: duration });