mirror of https://github.com/vector-im/riot-web
Merge pull request #6251 from SimonBrandner/ts/entity-and-member-tile
commit
8f6d31b73c
|
@ -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;
|
|
|
@ -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
|
|
@ -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 });
|
Loading…
Reference in New Issue