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:
@@ -16,37 +16,49 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {Filter} from 'matrix-js-sdk/src/filter';
|
||||
import { Filter } from 'matrix-js-sdk/src/filter';
|
||||
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
||||
|
||||
import * as sdk from '../../index';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import EventIndexPeg from "../../indexing/EventIndexPeg";
|
||||
import { _t } from '../../languageHandler';
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onClose: () => void;
|
||||
resizeNotifier: ResizeNotifier
|
||||
}
|
||||
|
||||
interface IState {
|
||||
timelineSet: EventTimelineSet;
|
||||
}
|
||||
|
||||
/*
|
||||
* Component which shows the filtered file using a TimelinePanel
|
||||
*/
|
||||
@replaceableComponent("structures.FilePanel")
|
||||
class FilePanel extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class FilePanel extends React.Component<IProps, IState> {
|
||||
// This is used to track if a decrypted event was a live event and should be
|
||||
// added to the timeline.
|
||||
decryptingEvents = new Set();
|
||||
private decryptingEvents = new Set<string>();
|
||||
public noRoom: boolean;
|
||||
|
||||
state = {
|
||||
timelineSet: null,
|
||||
};
|
||||
|
||||
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
|
||||
private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: true, removed: true, data: any): void => {
|
||||
if (room?.roomId !== this.props?.roomId) return;
|
||||
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||
|
||||
@@ -60,7 +72,7 @@ class FilePanel extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
onEventDecrypted = (ev, err) => {
|
||||
private onEventDecrypted = (ev: MatrixEvent, err?: any): void => {
|
||||
if (ev.getRoomId() !== this.props.roomId) return;
|
||||
const eventId = ev.getId();
|
||||
|
||||
@@ -70,7 +82,7 @@ class FilePanel extends React.Component {
|
||||
this.addEncryptedLiveEvent(ev);
|
||||
};
|
||||
|
||||
addEncryptedLiveEvent(ev, toStartOfTimeline) {
|
||||
public addEncryptedLiveEvent(ev: MatrixEvent): void {
|
||||
if (!this.state.timelineSet) return;
|
||||
|
||||
const timeline = this.state.timelineSet.getLiveTimeline();
|
||||
@@ -84,7 +96,7 @@ class FilePanel extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
public async componentDidMount(): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
await this.updateTimelineSet(this.props.roomId);
|
||||
@@ -105,7 +117,7 @@ class FilePanel extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client === null) return;
|
||||
|
||||
@@ -117,7 +129,7 @@ class FilePanel extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchFileEventsServer(room) {
|
||||
public async fetchFileEventsServer(room: Room): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const filter = new Filter(client.credentials.userId);
|
||||
@@ -141,7 +153,7 @@ class FilePanel extends React.Component {
|
||||
return timelineSet;
|
||||
}
|
||||
|
||||
onPaginationRequest = (timelineWindow, direction, limit) => {
|
||||
private onPaginationRequest = (timelineWindow: TimelineWindow, direction: string, limit: number): void => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const eventIndex = EventIndexPeg.get();
|
||||
const roomId = this.props.roomId;
|
||||
@@ -159,7 +171,7 @@ class FilePanel extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
async updateTimelineSet(roomId: string) {
|
||||
public async updateTimelineSet(roomId: string): Promise<void> {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
const eventIndex = EventIndexPeg.get();
|
||||
@@ -195,7 +207,7 @@ class FilePanel extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return <BaseCard
|
||||
className="mx_FilePanel mx_RoomView_messageListWrapper"
|
||||
@@ -24,6 +24,7 @@ import CustomRoomTagPanel from "./CustomRoomTagPanel";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
import RoomList from "../views/rooms/RoomList";
|
||||
import CallHandler from "../../CallHandler";
|
||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import UserMenu from "./UserMenu";
|
||||
@@ -124,6 +125,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
this.setState({ activeSpace });
|
||||
};
|
||||
|
||||
private onDialPad = () => {
|
||||
dis.fire(Action.OpenDialPad);
|
||||
}
|
||||
|
||||
private onExplore = () => {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
@@ -397,7 +402,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
private renderSearchExplore(): React.ReactNode {
|
||||
private renderSearchDialExplore(): React.ReactNode {
|
||||
let dialPadButton = null;
|
||||
|
||||
// If we have dialer support, show a button to bring up the dial pad
|
||||
// to start a new call
|
||||
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||
dialPadButton =
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_dialPadButton", {})}
|
||||
onClick={this.onDialPad}
|
||||
title={_t("Open dial pad")}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mx_LeftPanel_filterContainer"
|
||||
@@ -410,6 +428,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
onKeyDown={this.onKeyDown}
|
||||
onSelectRoom={this.selectRoom}
|
||||
/>
|
||||
|
||||
{dialPadButton}
|
||||
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_exploreButton", {
|
||||
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
|
||||
@@ -458,7 +479,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
{leftLeftPanel}
|
||||
<aside className="mx_LeftPanel_roomListContainer">
|
||||
{this.renderHeader()}
|
||||
{this.renderSearchExplore()}
|
||||
{this.renderSearchDialExplore()}
|
||||
{this.renderBreadcrumbs()}
|
||||
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||
<div className="mx_LeftPanel_roomListWrapper">
|
||||
|
||||
@@ -50,6 +50,7 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
|
||||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
@@ -78,10 +79,6 @@ function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): b
|
||||
return true;
|
||||
}
|
||||
|
||||
const isMembershipChange = (e: MatrixEvent): boolean => {
|
||||
return e.getType() === EventType.RoomMember || e.getType() === EventType.RoomThirdPartyInvite;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// the list of MatrixEvents to display
|
||||
events: MatrixEvent[];
|
||||
@@ -1183,7 +1180,7 @@ class RedactionGrouper extends BaseGrouper {
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
class MemberGrouper extends BaseGrouper {
|
||||
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
|
||||
return panel.shouldShowEvent(ev) && isMembershipChange(ev);
|
||||
return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -1200,7 +1197,7 @@ class MemberGrouper extends BaseGrouper {
|
||||
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||
return false;
|
||||
}
|
||||
return isMembershipChange(ev);
|
||||
return membershipTypes.includes(ev.getType() as EventType);
|
||||
}
|
||||
|
||||
public add(ev: MatrixEvent): void {
|
||||
|
||||
@@ -82,6 +82,7 @@ import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
@@ -192,6 +193,7 @@ export interface IState {
|
||||
// whether or not a spaces context switch brought us here,
|
||||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
wasContextSwitch?: boolean;
|
||||
editState?: EditorStateTransfer;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
@@ -815,6 +817,36 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
case 'focus_search':
|
||||
this.onSearchClick();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
this.messagePanel?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case Action.ComposerInsert: {
|
||||
// re-dispatch to the correct composer
|
||||
if (this.state.editState) {
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: "edit_composer_insert",
|
||||
});
|
||||
} else {
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: "send_composer_insert",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "scroll_to_bottom":
|
||||
this.messagePanel?.jumpToLiveTimeline();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2040,6 +2072,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
layout={this.state.layout}
|
||||
editState={this.state.editState}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
||||
@@ -14,58 +14,60 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {RefObject, useContext, useRef, useState} from "react";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import React, { RefObject, useContext, useRef, useState } from "react";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||
import {_t} from "../../languageHandler";
|
||||
import { _t } from "../../languageHandler";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import RoomTopic from "../views/elements/RoomTopic";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||
import createRoom, {IOpts} from "../../createRoom";
|
||||
import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
|
||||
import { useRoomMembers } from "../../hooks/useRoomMembers";
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||
import { useEventEmitter } from "../../hooks/useEventEmitter";
|
||||
import withValidation from "../views/elements/Validation";
|
||||
import * as Email from "../../email";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier"
|
||||
import MainSplit from './MainSplit';
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import {ActionPayload} from "../../dispatcher/payloads";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import RightPanel from "./RightPanel";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {useStateArray} from "../../hooks/useStateArray";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { useStateArray } from "../../hooks/useStateArray";
|
||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
|
||||
import {showRoom, SpaceHierarchy} from "./SpaceRoomDirectory";
|
||||
import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space";
|
||||
import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import FacePile from "../views/elements/FacePile";
|
||||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
||||
import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog";
|
||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import {BetaPill} from "../views/beta/BetaCard";
|
||||
import { BetaPill } from "../views/beta/BetaCard";
|
||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
@@ -178,6 +180,9 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||
|
||||
const spacesEnabled = SettingsStore.getValue("feature_spaces");
|
||||
|
||||
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
|
||||
&& space.getJoinRule() !== JoinRule.Public;
|
||||
|
||||
let inviterSection;
|
||||
let joinButtons;
|
||||
if (myMembership === "join") {
|
||||
@@ -244,7 +249,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||
setBusy(true);
|
||||
onJoinButtonClicked();
|
||||
}}
|
||||
disabled={!spacesEnabled}
|
||||
disabled={!spacesEnabled || cannotJoin}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
@@ -255,6 +260,30 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||
joinButtons = <InlineSpinner />;
|
||||
}
|
||||
|
||||
let footer;
|
||||
if (!spacesEnabled) {
|
||||
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||
{ myMembership === "join"
|
||||
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||
spaceName: space.name,
|
||||
}, {
|
||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||
})
|
||||
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||
spaceName: space.name,
|
||||
}, {
|
||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
} else if (cannotJoin) {
|
||||
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||
{ _t("To view %(spaceName)s, you need an invite", {
|
||||
spaceName: space.name,
|
||||
}) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_preview">
|
||||
<BetaPill onClick={onBetaClick} />
|
||||
{ inviterSection }
|
||||
@@ -274,20 +303,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||
<div className="mx_SpaceRoomView_preview_joinButtons">
|
||||
{ joinButtons }
|
||||
</div>
|
||||
{ !spacesEnabled && <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||
{ myMembership === "join"
|
||||
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||
spaceName: space.name,
|
||||
}, {
|
||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||
})
|
||||
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||
spaceName: space.name,
|
||||
}, {
|
||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||
})
|
||||
}
|
||||
</div> }
|
||||
{ footer }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,12 +34,10 @@ import * as sdk from "../../index";
|
||||
import { Key } from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import { haveTileForEvent, TileShape } from "../views/rooms/EventTile";
|
||||
import { UIFeature } from "../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import MessagePanel from "./MessagePanel";
|
||||
import { SyncState } from 'matrix-js-sdk/src/sync.api';
|
||||
import { IScrollState } from "./ScrollPanel";
|
||||
@@ -48,6 +46,7 @@ import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
@@ -494,38 +493,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
case "ignore_state_changed":
|
||||
this.forceUpdate();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event && this.messagePanel.current) {
|
||||
this.messagePanel.current.scrollToEventIfNeeded(
|
||||
payload.event.getId(),
|
||||
);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case Action.ComposerInsert: {
|
||||
// re-dispatch to the correct composer
|
||||
if (this.state.editState) {
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: "edit_composer_insert",
|
||||
});
|
||||
} else {
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
action: "send_composer_insert",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "scroll_to_bottom":
|
||||
this.jumpToLiveTimeline();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -925,6 +892,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
public scrollToEventIfNeeded = (eventId: string): void => {
|
||||
this.messagePanel.current?.scrollToEventIfNeeded(eventId);
|
||||
}
|
||||
|
||||
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
|
||||
* the container.
|
||||
*/
|
||||
@@ -1526,7 +1497,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
tileShape={this.props.tileShape}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editState={this.state.editState}
|
||||
editState={this.props.editState}
|
||||
showReactions={this.props.showReactions}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+47
-46
@@ -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;
|
||||
}
|
||||
|
||||
+44
-34
@@ -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
|
||||
+24
-27
@@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user