Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/ts/8
Conflicts: src/components/structures/MessagePanel.tsx src/components/structures/TimelinePanel.tsx
This commit is contained in:
commit
623317cd47
76 changed files with 1524 additions and 719 deletions
|
@ -28,7 +28,7 @@ import Resend from '../../../Resend';
|
|||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import { MenuItem } from "../../structures/ContextMenu";
|
||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||
|
@ -257,55 +257,68 @@ export default class MessageContextMenu extends React.Component {
|
|||
let externalURLButton;
|
||||
let quoteButton;
|
||||
let collapseReplyThread;
|
||||
let redactItemList;
|
||||
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
if (!mxEvent.isRedacted()) {
|
||||
if (unsentReactionsCount !== 0) {
|
||||
resendReactionsButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
||||
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconResend"
|
||||
label={ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
||||
onClick={this.onResendReactionsClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconRedact"
|
||||
label={_t("Remove")}
|
||||
onClick={this.onRedactClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isContentActionable(mxEvent)) {
|
||||
forwardButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
{ _t('Forward Message') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconForward"
|
||||
label={_t("Forward")}
|
||||
onClick={this.onForwardClick}
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconPin"
|
||||
label={ this._isPinned() ? _t('Unpin') : _t('Pin') }
|
||||
onClick={this.onPinClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const viewSourceButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
{ _t('View Source') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconSource"
|
||||
label={_t("View source")}
|
||||
onClick={this.onViewSourceClick}
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.eventTileOps) {
|
||||
if (this.props.eventTileOps.isWidgetHidden()) {
|
||||
unhidePreviewButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
{ _t('Unhide Preview') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
|
||||
label={_t("Show preview")}
|
||||
onClick={this.onUnhidePreviewClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -316,77 +329,97 @@ export default class MessageContextMenu extends React.Component {
|
|||
}
|
||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||
const permalinkButton = (
|
||||
<MenuItem
|
||||
element="a"
|
||||
className="mx_MessageContextMenu_field"
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconPermalink"
|
||||
onClick={this.onPermalinkClick}
|
||||
label= {_t('Share')}
|
||||
element="a"
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
</MenuItem>
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.eventTileOps) { // this event is rendered using TextualBody
|
||||
quoteButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
{ _t('Quote') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconQuote"
|
||||
label={_t("Quote")}
|
||||
onClick={this.onQuoteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Bridges can provide a 'external_url' to link back to the source.
|
||||
if (
|
||||
typeof(mxEvent.event.content.external_url) === "string" &&
|
||||
if (typeof (mxEvent.event.content.external_url) === "string" &&
|
||||
isUrlPermitted(mxEvent.event.content.external_url)
|
||||
) {
|
||||
externalURLButton = (
|
||||
<MenuItem
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconLink"
|
||||
onClick={this.closeMenu}
|
||||
label={ _t('Source URL') }
|
||||
element="a"
|
||||
className="mx_MessageContextMenu_field"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
onClick={this.closeMenu}
|
||||
href={mxEvent.event.content.external_url}
|
||||
>
|
||||
{ _t('Source URL') }
|
||||
</MenuItem>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.collapseReplyThread) {
|
||||
collapseReplyThread = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||
{ _t('Collapse Reply Thread') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconCollapse"
|
||||
label={_t("Collapse reply thread")}
|
||||
onClick={this.onCollapseReplyThreadClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
|
||||
{ _t('Report Content') }
|
||||
</MenuItem>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconReport"
|
||||
label={_t("Report")}
|
||||
onClick={this.onReportEventClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const commonItemsList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ quoteButton }
|
||||
{ forwardButton }
|
||||
{ pinButton }
|
||||
{ permalinkButton }
|
||||
{ reportEventButton }
|
||||
{ externalURLButton }
|
||||
{ unhidePreviewButton }
|
||||
{ viewSourceButton }
|
||||
{ resendReactionsButton }
|
||||
{ collapseReplyThread }
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
|
||||
if (redactButton) {
|
||||
redactItemList = (
|
||||
<IconizedContextMenuOptionList red>
|
||||
{ redactButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MessageContextMenu">
|
||||
{ resendReactionsButton }
|
||||
{ redactButton }
|
||||
{ forwardButton }
|
||||
{ pinButton }
|
||||
{ viewSourceButton }
|
||||
{ unhidePreviewButton }
|
||||
{ permalinkButton }
|
||||
{ quoteButton }
|
||||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
{ reportEventButton }
|
||||
</div>
|
||||
<IconizedContextMenu
|
||||
{...this.props}
|
||||
className="mx_MessageContextMenu"
|
||||
compact={true}
|
||||
>
|
||||
{ commonItemsList }
|
||||
{ redactItemList }
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ enum TransitionType {
|
|||
ChangedName = "changed_name",
|
||||
ChangedAvatar = "changed_avatar",
|
||||
NoChange = "no_change",
|
||||
ServerAcl = "server_acl",
|
||||
}
|
||||
|
||||
const SEP = ",";
|
||||
|
@ -288,6 +289,12 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "server_acl":
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)schanged the server ACLs %(count)s times",
|
||||
{ severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
|
@ -314,6 +321,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
return TransitionType.Invited;
|
||||
}
|
||||
|
||||
if (e.mxEvent.getType() === 'm.room.server_acl') {
|
||||
return TransitionType.ServerAcl;
|
||||
}
|
||||
|
||||
switch (e.mxEvent.getContent().membership) {
|
||||
case 'invite': return TransitionType.Invited;
|
||||
case 'ban': return TransitionType.Banned;
|
||||
|
@ -400,19 +411,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
|||
// Object mapping user IDs to an array of IUserEvents
|
||||
const userEvents: Record<string, IUserEvents[]> = {};
|
||||
eventsToRender.forEach((e, index) => {
|
||||
const userId = e.getStateKey();
|
||||
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
|
||||
// Initialise a user's events
|
||||
if (!userEvents[userId]) {
|
||||
userEvents[userId] = [];
|
||||
}
|
||||
|
||||
if (e.target) {
|
||||
if (e.getType() === 'm.room.server_acl') {
|
||||
latestUserAvatarMember.set(userId, e.sender);
|
||||
} else if (e.target) {
|
||||
latestUserAvatarMember.set(userId, e.target);
|
||||
}
|
||||
|
||||
let displayName = userId;
|
||||
if (e.getType() === 'm.room.third_party_invite') {
|
||||
displayName = e.getContent().display_name;
|
||||
} else if (e.getType() === 'm.room.server_acl') {
|
||||
displayName = e.sender.name;
|
||||
} else if (e.target) {
|
||||
displayName = e.target.name;
|
||||
}
|
||||
|
|
|
@ -48,15 +48,14 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
|
|||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<MessageContextMenu
|
||||
mxEvent={mxEvent}
|
||||
permalinkCreator={permalinkCreator}
|
||||
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
|
||||
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
|
||||
onFinished={closeMenu}
|
||||
/>
|
||||
</ContextMenu>;
|
||||
contextMenu = <MessageContextMenu
|
||||
{...aboveLeftOf(buttonRect)}
|
||||
mxEvent={mxEvent}
|
||||
permalinkCreator={permalinkCreator}
|
||||
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
|
||||
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
|
||||
onFinished={closeMenu}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
|
|
|
@ -140,7 +140,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
||||
<span className={`mx_SenderProfile_displayName ${colorClass}`}>
|
||||
{ displayName }
|
||||
</span>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -853,7 +853,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
let tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
||||
let isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
||||
(eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
|
||||
(eventType === EventType.RoomCreate) ||
|
||||
(eventType === EventType.RoomEncryption) ||
|
||||
|
@ -869,6 +869,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
// duplicate of the thing they are replacing).
|
||||
if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) {
|
||||
tileHandler = "messages.ViewSourceEvent";
|
||||
isBubbleMessage = false;
|
||||
// Reuse info message avatar and sender profile styling
|
||||
isInfoMessage = true;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,19 +15,26 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import React, {ReactNode} from "react";
|
||||
import {RecordingState, VoiceRecording} from "../../../voice/VoiceRecording";
|
||||
import {
|
||||
IRecordingUpdate,
|
||||
RECORDING_PLAYBACK_SAMPLES,
|
||||
RecordingState,
|
||||
VoiceRecording,
|
||||
} from "../../../voice/VoiceRecording";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||
import { percentageOf } from "../../../utils/numbers";
|
||||
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
|
||||
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
|
||||
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||
import {MsgType} from "matrix-js-sdk/src/@types/event";
|
||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||
|
@ -39,6 +46,8 @@ interface IProps {
|
|||
interface IState {
|
||||
recorder?: VoiceRecording;
|
||||
recordingPhase?: RecordingState;
|
||||
relHeights: number[];
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,18 +55,58 @@ interface IState {
|
|||
*/
|
||||
@replaceableComponent("views.rooms.VoiceRecordComposerTile")
|
||||
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
|
||||
private waveform: number[] = [];
|
||||
private seconds = 0;
|
||||
private scheduledAnimationFrame = false;
|
||||
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
recorder: null, // no recording started by default
|
||||
seconds: 0,
|
||||
relHeights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps, prevState) {
|
||||
if (!prevState.recorder && this.state.recorder) {
|
||||
this.state.recorder.liveData.onUpdate(this.onRecordingUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public async componentWillUnmount() {
|
||||
await VoiceRecordingStore.instance.disposeRecording();
|
||||
}
|
||||
|
||||
private onRecordingUpdate = (update: IRecordingUpdate): void => {
|
||||
this.waveform = update.waveform;
|
||||
this.seconds = update.timeSeconds;
|
||||
|
||||
if (this.scheduledAnimationFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scheduledAnimationFrame = true;
|
||||
// The audio recorder flushes data faster than the screen refresh rate
|
||||
// Using requestAnimationFrame makes sure that we only flush the data
|
||||
// to react once per tick to avoid unneeded work.
|
||||
requestAnimationFrame(() => {
|
||||
// The waveform and the downsample target are pretty close, so we should be fine to
|
||||
// do this, despite the docs on arrayFastResample.
|
||||
const bars = arrayFastResample(Array.from(this.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||
this.setState({
|
||||
// The incoming data is between zero and one, but typically even screaming into a
|
||||
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
||||
// waveform. This results in a slightly more cinematic/animated waveform for the
|
||||
// user.
|
||||
relHeights: bars.map(b => percentageOf(b, 0, 0.50)),
|
||||
seconds: this.seconds,
|
||||
});
|
||||
this.scheduledAnimationFrame = false;
|
||||
});
|
||||
}
|
||||
|
||||
// called by composer
|
||||
public async send() {
|
||||
if (!this.state.recorder) {
|
||||
|
@ -65,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,
|
||||
|
@ -81,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,
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
export interface IProps {
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
@ -15,12 +12,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import Clock from "./Clock";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
import {
|
||||
IRecordingUpdate,
|
||||
VoiceRecording,
|
||||
} from "../../../voice/VoiceRecording";
|
||||
|
||||
interface IProps {
|
||||
recorder: VoiceRecording;
|
||||
recorder?: VoiceRecording;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -32,16 +33,31 @@ interface IState {
|
|||
*/
|
||||
@replaceableComponent("views.voice_messages.LiveRecordingClock")
|
||||
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
private seconds = 0;
|
||||
private scheduledUpdate = new MarkedExecution(
|
||||
() => this.updateClock(),
|
||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||
);
|
||||
|
||||
this.state = {seconds: 0};
|
||||
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
seconds: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private onRecordingUpdate = (update: IRecordingUpdate) => {
|
||||
this.setState({seconds: update.timeSeconds});
|
||||
};
|
||||
componentDidMount() {
|
||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||
this.seconds = update.timeSeconds;
|
||||
this.scheduledUpdate.mark();
|
||||
});
|
||||
}
|
||||
|
||||
private updateClock() {
|
||||
this.setState({
|
||||
seconds: this.seconds,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <Clock seconds={this.state.seconds} />;
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
@ -15,18 +12,20 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {arrayFastResample, arraySeed} from "../../../utils/arrays";
|
||||
import {percentageOf} from "../../../utils/numbers";
|
||||
import Waveform from "./Waveform";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
import {
|
||||
IRecordingUpdate,
|
||||
VoiceRecording,
|
||||
} from "../../../voice/VoiceRecording";
|
||||
|
||||
interface IProps {
|
||||
recorder: VoiceRecording;
|
||||
recorder?: VoiceRecording;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
heights: number[];
|
||||
waveform: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,27 +33,37 @@ interface IState {
|
|||
*/
|
||||
@replaceableComponent("views.voice_messages.LiveRecordingWaveform")
|
||||
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)};
|
||||
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
|
||||
}
|
||||
|
||||
private onRecordingUpdate = (update: IRecordingUpdate) => {
|
||||
// The waveform and the downsample target are pretty close, so we should be fine to
|
||||
// do this, despite the docs on arrayFastResample.
|
||||
const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||
this.setState({
|
||||
// The incoming data is between zero and one, but typically even screaming into a
|
||||
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
||||
// waveform. This results in a slightly more cinematic/animated waveform for the
|
||||
// user.
|
||||
heights: bars.map(b => percentageOf(b, 0, 0.50)),
|
||||
});
|
||||
public static defaultProps = {
|
||||
progress: 1,
|
||||
};
|
||||
|
||||
private waveform: number[] = [];
|
||||
private scheduledUpdate = new MarkedExecution(
|
||||
() => this.updateWaveform(),
|
||||
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
|
||||
);
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
waveform: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
|
||||
this.waveform = update.waveform;
|
||||
this.scheduledUpdate.mark();
|
||||
});
|
||||
}
|
||||
|
||||
private updateWaveform() {
|
||||
this.setState({
|
||||
waveform: this.waveform,
|
||||
})
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <Waveform relHeights={this.state.heights} />;
|
||||
return <Waveform relHeights={this.state.waveform} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface IProps {
|
||||
export interface IProps {
|
||||
relHeights: number[]; // relative heights (0-1)
|
||||
progress: number; // percent complete, 0-1, default 100%
|
||||
}
|
||||
|
@ -34,16 +34,19 @@ interface IState {
|
|||
* For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be
|
||||
* "filled", as a demonstration of the progress property.
|
||||
*/
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export interface WaveformCSSProperties extends CSSProperties {
|
||||
'--barHeight': number;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.voice_messages.Waveform")
|
||||
export default class Waveform extends React.PureComponent<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
progress: 1,
|
||||
};
|
||||
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div className='mx_Waveform'>
|
||||
{this.props.relHeights.map((h, i) => {
|
||||
|
@ -53,7 +56,9 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
|
|||
'mx_Waveform_bar': true,
|
||||
'mx_Waveform_bar_100pct': isCompleteBar,
|
||||
});
|
||||
return <span key={i} style={{height: (h * 100) + '%'}} className={classes} />;
|
||||
return <span key={i} style={{
|
||||
"--barHeight": h,
|
||||
} as WaveformCSSProperties} className={classes} />;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue