Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17365

This commit is contained in:
Michael Telatynski 2021-06-08 16:54:49 +01:00
commit f34a2e61bc
95 changed files with 2180 additions and 1311 deletions

View file

@ -358,7 +358,7 @@ class LoggedInView extends React.Component<IProps, IState> {
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) events.push(event);
}

View file

@ -378,7 +378,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.onLoggedIn();
}
const promisesList = [this.firstSyncPromise.promise];
const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account

View file

@ -19,7 +19,6 @@ limitations under the License.
import React, {createRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils';
import * as sdk from '../../index';
@ -616,10 +615,6 @@ export default class MessagePanel extends React.Component {
const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId);
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
const scrollToken = mxEv.status ? undefined : eventId;
const readReceipts = this._readReceiptsByEvent[eventId];
let isLastSuccessful = false;
@ -651,7 +646,6 @@ export default class MessagePanel extends React.Component {
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
<EventTile
as="li"
data-scroll-tokens={scrollToken}
ref={this._collectEventNode.bind(this, eventId)}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
mxEvent={mxEv}
@ -854,13 +848,6 @@ export default class MessagePanel extends React.Component {
const style = this.props.hidden ? { display: 'none' } : {};
const className = classNames(
this.props.className,
{
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
},
);
let whoIsTyping;
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (<WhoIsTypingTile
@ -884,7 +871,7 @@ export default class MessagePanel extends React.Component {
<ErrorBoundary>
<ScrollPanel
ref={this._scrollPanel}
className={className}
className={this.props.className}
onScroll={this.props.onScroll}
onUserScroll={this.props.onUserScroll}
onResize={this.onResize}

View file

@ -1,7 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2016, 2019, 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.
@ -16,29 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from "prop-types";
import React from "react";
import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseCard from "../views/right_panel/BaseCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
interface IProps {
onClose(): void;
}
/*
* Component which shows the global notification list using a TimelinePanel
*/
@replaceableComponent("structures.NotificationPanel")
class NotificationPanel extends React.Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
};
export default class NotificationPanel extends React.PureComponent<IProps> {
render() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{_t('Youre all caught up')}</h2>
<p>{_t('You have no visible notifications.')}</p>
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
let content;
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
// wrap a TimelinePanel with the jump-to-event bits turned off.
content = (
<TimelinePanel
manageReadReceipts={false}
@ -55,11 +50,12 @@ class NotificationPanel extends React.Component {
showUrlPreview={false}
tileShape="notif"
empty={emptyState}
alwaysShowTimestamps={true}
/>
);
} else {
console.error("No notifTimelineSet available!");
content = <Loader />;
content = <Spinner />;
}
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
@ -67,5 +63,3 @@ class NotificationPanel extends React.Component {
</BaseCard>;
}
}
export default NotificationPanel;

View file

@ -1,6 +1,6 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
Copyright 2015 - 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.
@ -16,70 +16,92 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {Room} from "matrix-js-sdk/src/models/room";
import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import {
RightPanelPhases,
RIGHT_PANEL_PHASES_NO_ARGS,
RIGHT_PANEL_SPACE_PHASES,
RightPanelPhases,
} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
import { Action } from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SettingsStore from "../../settings/SettingsStore";
import { ActionPayload } from "../../dispatcher/payloads";
import MemberList from "../views/rooms/MemberList";
import GroupMemberList from "../views/groups/GroupMemberList";
import GroupRoomList from "../views/groups/GroupRoomList";
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
import UserInfo from "../views/right_panel/UserInfo";
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
import FilePanel from "./FilePanel";
import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
interface IProps {
room?: Room; // if showing panels for a given room, this is set
groupId?: string; // if showing panels for a given group, this is set
user?: User; // used if we know the user ahead of opening the panel
resizeNotifier: ResizeNotifier;
}
interface IState {
phase: RightPanelPhases;
isUserPrivilegedInGroup?: boolean;
member?: RoomMember;
verificationRequest?: VerificationRequest;
verificationRequestPromise?: Promise<VerificationRequest>;
space?: Room;
widgetId?: string;
groupRoomId?: string;
groupId?: string;
event: MatrixEvent;
}
@replaceableComponent("structures.RightPanel")
export default class RightPanel extends React.Component {
static get propTypes() {
return {
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
groupId: PropTypes.string, // if showing panels for a given group, this is set
user: PropTypes.object, // used if we know the user ahead of opening the panel
};
}
export default class RightPanel extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
private readonly delayedUpdate: RateLimitedFunc;
private dispatcherRef: string;
constructor(props, context) {
super(props, context);
this.state = {
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
phase: this._getPhaseFromProps(),
phase: this.getPhaseFromProps(),
isUserPrivilegedInGroup: null,
member: this._getUserForPanel(),
member: this.getUserForPanel(),
};
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
this._delayedUpdate = new RateLimitedFunc(() => {
this.delayedUpdate = new RateLimitedFunc(() => {
this.forceUpdate();
}, 500);
}
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
// Helper function to split out the logic for getPhaseFromProps() and the constructor
// as both are called at the same time in the constructor.
_getUserForPanel() {
private getUserForPanel() {
if (this.state && this.state.member) return this.state.member;
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
return this.props.user || lastParams['member'];
}
// gets the current phase from the props and also maybe the store
_getPhaseFromProps() {
private getPhaseFromProps() {
const rps = RightPanelStore.getSharedInstance();
const userForPanel = this._getUserForPanel();
const userForPanel = this.getUserForPanel();
if (this.props.groupId) {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
this.dispatcherRef = dis.register(this.onAction);
const cli = this.context;
cli.on("RoomState.members", this.onRoomStateMember);
this._initGroupStore(this.props.groupId);
this.initGroupStore(this.props.groupId);
}
componentWillUnmount() {
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
if (this.context) {
this.context.removeListener("RoomState.members", this.onRoomStateMember);
}
this._unregisterGroupStore(this.props.groupId);
this.unregisterGroupStore();
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
this.unregisterGroupStore();
this.initGroupStore(newProps.groupId);
}
}
_initGroupStore(groupId) {
private initGroupStore(groupId: string) {
if (!groupId) return;
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
}
_unregisterGroupStore() {
private unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
}
onGroupStoreUpdated() {
private onGroupStoreUpdated = () => {
this.setState({
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
}
};
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
this.setState({
phase: RightPanelPhases.GroupMemberList,
});
});
}
onAddRoomToGroupButtonClick() {
showGroupAddRoomDialog(this.props.groupId).then(() => {
this.forceUpdate();
});
}
onRoomStateMember(ev, state, member) {
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
if (!this.props.room || member.roomId !== this.props.room.roomId) {
return;
}
// redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
this._delayedUpdate();
this.delayedUpdate();
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level)
this._delayedUpdate();
this.delayedUpdate();
}
}
};
onAction(payload) {
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.AfterRightPanelPhaseChange) {
this.setState({
phase: payload.phase,
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
space: payload.space,
});
}
}
};
onClose = () => {
private onClose = () => {
// XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly.
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
};
render() {
const MemberList = sdk.getComponent('rooms.MemberList');
const UserInfo = sdk.getComponent('right_panel.UserInfo');
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
const FilePanel = sdk.getComponent('structures.FilePanel');
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
let panel = <div />;
const roomId = this.props.room ? this.props.room.roomId : undefined;
@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
user={this.state.member}
groupId={this.props.groupId}
key={this.state.member.userId}
phase={this.state.phase}
onClose={this.onClose} />;
break;
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
panel = <NotificationPanel onClose={this.onClose} />;
break;
case RightPanelPhases.PinnedMessages:
if (SettingsStore.getValue("feature_pinning")) {
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
}
break;
case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
break;

View file

@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import {Layout} from "../../settings/Layout";
import { Layout } from "../../settings/Layout";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import { haveTileForEvent } from "../views/rooms/EventTile";
@ -54,16 +54,13 @@ import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
import { Action } from "../../dispatcher/actions";
import { SettingLevel } from "../../settings/SettingLevel";
import { IMatrixClientCreds } from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
import ForwardMessage from "../views/rooms/ForwardMessage";
import SearchBar from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
import { XOR } from "../../@types/common";
@ -82,7 +79,7 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { omit } from 'lodash';
import UIStore from "../../stores/UIStore";
@ -138,7 +135,6 @@ export interface IState {
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent;
numUnreadMessages: number;
draggingFile: boolean;
searching: boolean;
@ -157,7 +153,6 @@ export interface IState {
canPeek: boolean;
showApps: boolean;
isPeeking: boolean;
showingPinned: boolean;
showReadReceipts: boolean;
showRightPanel: boolean;
// error object, as from the matrix client/server API
@ -235,7 +230,6 @@ export default class RoomView extends React.Component<IProps, IState> {
canPeek: false,
showApps: false,
isPeeking: false,
showingPinned: false,
showReadReceipts: true,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
joining: false,
@ -327,10 +321,8 @@ export default class RoomView extends React.Component<IProps, IState> {
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
};
@ -1403,13 +1395,6 @@ export default class RoomView extends React.Component<IProps, IState> {
return ret;
}
private onPinnedClick = () => {
const nowShowingPinned = !this.state.showingPinned;
const roomId = this.state.room.roomId;
this.setState({showingPinned: nowShowingPinned, searching: false});
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
};
private onCallPlaced = (type: PlaceCallType) => {
dis.dispatch({
action: 'place_call',
@ -1422,18 +1407,6 @@ export default class RoomView extends React.Component<IProps, IState> {
dis.dispatch({ action: "open_room_settings" });
};
private onCancelClick = () => {
console.log("updateTint from onCancelClick");
this.updateTint();
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.fire(Action.FocusComposer);
};
private onAppsClick = () => {
dis.dispatch({
action: "appsDrawer",
@ -1526,7 +1499,6 @@ export default class RoomView extends React.Component<IProps, IState> {
private onSearchClick = () => {
this.setState({
searching: !this.state.searching,
showingPinned: false,
});
};
@ -1539,10 +1511,19 @@ export default class RoomView extends React.Component<IProps, IState> {
// jump down to the bottom of this room, where new events are arriving
private jumpToLiveTimeline = () => {
dis.dispatch({
action: 'view_room',
room_id: this.state.room.roomId,
});
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
// If we were viewing a highlighted event, firing view_room without
// an event will take care of both clearing the URL fragment and
// jumping to the bottom
dis.dispatch({
action: 'view_room',
room_id: this.state.room.roomId,
});
} else {
// Otherwise we have to jump manually
this.messagePanel.jumpToLiveTimeline();
dis.fire(Action.FocusComposer);
}
};
// jump up to wherever our read marker is
@ -1841,11 +1822,7 @@ export default class RoomView extends React.Component<IProps, IState> {
let aux = null;
let previewBar;
let hideCancel = false;
if (this.state.forwardingEvent) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
if (this.state.searching) {
aux = <SearchBar
searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick}
@ -1854,10 +1831,6 @@ export default class RoomView extends React.Component<IProps, IState> {
/>;
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
} else if (this.state.showingPinned) {
hideCancel = true; // has own cancel
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
} else if (myMembership !== "join") {
// We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it.
@ -1866,7 +1839,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inviterName = this.props.oobData.inviterName;
}
const invitedEmail = this.props.threepidInvite?.toEmail;
hideCancel = true;
previewBar = (
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
@ -1984,11 +1956,8 @@ export default class RoomView extends React.Component<IProps, IState> {
hideMessagePanel = true;
}
const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null;
if (this.state.forwardingEvent) {
highlightedEventId = this.state.forwardingEvent.getId();
} else if (shouldHighlight) {
if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.initialEventId;
}
@ -2077,8 +2046,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}

View file

@ -587,6 +587,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton>
<div className="mx_SpaceRoomView_betaWarning">
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
</div>
<SpaceFeedbackPrompt />
</div>;
};

View file

@ -120,6 +120,9 @@ class TimelinePanel extends React.Component {
// which layout to use
layout: LayoutPropType,
// whether to always show timestamps for an event
alwaysShowTimestamps: PropTypes.bool,
}
// a map from room id to read marker event timestamp
@ -1440,7 +1443,7 @@ class TimelinePanel extends React.Component {
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
className={this.props.className}
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}

View file

@ -61,7 +61,7 @@ interface IProps {
is_url?: string;
session_id: string;
/* eslint-enable camelcase */
}): void;
}): string;
// registration shouldn't know or care how login is done.
onLoginClick(): void;
onServerConfigChange(config: ValidatedServerConfig): void;

View file

@ -17,9 +17,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {EventStatus} from 'matrix-js-sdk/src/models/event';
import { EventStatus } from 'matrix-js-sdk/src/models/event';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -28,9 +28,11 @@ import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import {MenuItem} from "../../structures/ContextMenu";
import {EventType} from "matrix-js-sdk/src/@types/event";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { MenuItem } from "../../structures/ContextMenu";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
export function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -82,7 +84,7 @@ export default class MessageContextMenu extends React.Component {
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
@ -92,7 +94,7 @@ export default class MessageContextMenu extends React.Component {
_isPinned() {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
if (!pinnedEvent) return false;
const content = pinnedEvent.getContent();
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
@ -156,34 +158,32 @@ export default class MessageContextMenu extends React.Component {
};
onForwardClick = () => {
if (this.props.onCloseDialog) this.props.onCloseDialog();
dis.dispatch({
action: 'forward_event',
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
matrixClient: MatrixClientPeg.get(),
event: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
});
this.closeMenu();
};
onPinClick = () => {
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
.catch((e) => {
// Intercept the Event Not Found error and fall through the promise chain with no event.
if (e.errcode === "M_NOT_FOUND") return null;
throw e;
})
.then((event) => {
const eventIds = (event ? event.pinned : []) || [];
if (!eventIds.includes(this.props.mxEvent.getId())) {
// Not pinned - add
eventIds.push(this.props.mxEvent.getId());
} else {
// Pinned - remove
eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1);
}
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId();
const cli = MatrixClientPeg.get();
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
if (pinnedIds.includes(eventId)) {
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
} else {
pinnedIds.push(eventId);
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
event_ids: [
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
eventId,
],
});
}
cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
this.closeMenu();
};

View file

@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
showUnpin?: boolean;
// override delete handler
onDeleteClick?(): void;
// override edit handler
onEditClick?(): void;
}
const WidgetContextMenu: React.FC<IProps> = ({
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
app,
userWidget,
onDeleteClick,
onEditClick,
showUnpin,
...props
}) => {
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
let editButton;
if (canModify && WidgetUtils.isManagedByManager(app)) {
const onEditClick = () => {
WidgetUtils.editWidget(room, app);
const _onEditClick = () => {
if (onEditClick) {
onEditClick();
} else {
WidgetUtils.editWidget(room, app);
}
onFinished();
};
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
}
let snapshotButton;
@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC<IProps> = ({
let deleteButton;
if (onDeleteClick || canModify) {
const onDeleteClickDefault = () => {
// Show delete confirmation dialog
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
title: _t("Delete Widget"),
description: _t(
"Deleting a widget removes it for all users in this room." +
" Are you sure you want to delete this widget?"),
button: _t("Delete widget"),
onFinished: (confirmed) => {
if (!confirmed) return;
WidgetUtils.setRoomWidget(roomId, app.id);
},
});
const _onDeleteClick = () => {
if (onDeleteClick) {
onDeleteClick();
} else {
// Show delete confirmation dialog
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
title: _t("Delete Widget"),
description: _t(
"Deleting a widget removes it for all users in this room." +
" Are you sure you want to delete this widget?"),
button: _t("Delete widget"),
onFinished: (confirmed) => {
if (!confirmed) return;
WidgetUtils.setRoomWidget(roomId, app.id);
},
});
}
onFinished();
};
deleteButton = <IconizedContextMenuOption
onClick={onDeleteClick || onDeleteClickDefault}
onClick={_onDeleteClick}
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
/>;
}

View file

@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
render() {
const cli = this.context;
const room = this.props.room;
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
const inRoomChannel = cli.crypto._inRoomVerificationRequests;
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
return (<div>

View file

@ -0,0 +1,247 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>
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.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useMemo, useState, useEffect} from "react";
import classnames from "classnames";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {_t} from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
import {UIFeature} from "../../../settings/UIFeature";
import {Layout} from "../../../settings/Layout";
import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import {avatarUrlForUser} from "../../../Avatar";
import EventTile from "../rooms/EventTile";
import SearchBox from "../../structures/SearchBox";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {Alignment} from '../elements/Tooltip';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "../rooms/NotificationBadge";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
const AVATAR_SIZE = 30;
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
// The event to forward
event: MatrixEvent;
// We need a permalink creator for the source room to pass through to EventTile
// in case the event is a reply (even though the user can't get at the link)
permalinkCreator: RoomPermalinkCreator;
}
interface IEntryProps {
room: Room;
event: MatrixEvent;
matrixClient: MatrixClient;
onFinished(success: boolean): void;
}
enum SendState {
CanSend,
Sending,
Sent,
Failed,
}
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
const jumpToRoom = () => {
dis.dispatch({
action: "view_room",
room_id: room.roomId,
});
onFinished(true);
};
const send = async () => {
setSendState(SendState.Sending);
try {
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
setSendState(SendState.Sent);
} catch (e) {
setSendState(SendState.Failed);
}
};
let className;
let disabled = false;
let title;
let icon;
if (sendState === SendState.CanSend) {
className = "mx_ForwardList_canSend";
if (room.maySendMessage()) {
title = _t("Send");
} else {
disabled = true;
title = _t("You don't have permission to do this");
}
} else if (sendState === SendState.Sending) {
className = "mx_ForwardList_sending";
disabled = true;
title = _t("Sending");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
} else if (sendState === SendState.Sent) {
className = "mx_ForwardList_sent";
disabled = true;
title = _t("Sent");
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
} else {
className = "mx_ForwardList_sendFailed";
disabled = true;
title = _t("Failed to send");
icon = <NotificationBadge
notification={StaticNotificationState.RED_EXCLAMATION}
/>;
}
return <div className="mx_ForwardList_entry">
<AccessibleTooltipButton
className="mx_ForwardList_roomButton"
onClick={jumpToRoom}
title={_t("Open link")}
yOffset={-20}
alignment={Alignment.Top}
>
<DecoratedRoomAvatar room={room} avatarSize={32} />
<span className="mx_ForwardList_entry_name">{ room.name }</span>
</AccessibleTooltipButton>
<AccessibleTooltipButton
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
className={`mx_ForwardList_sendButton ${className}`}
onClick={send}
disabled={disabled}
title={title}
yOffset={-20}
alignment={Alignment.Top}
>
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
{ icon }
</AccessibleTooltipButton>
</div>;
};
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
const userId = cli.getUserId();
const [profileInfo, setProfileInfo] = useState<any>({});
useEffect(() => {
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
}, [cli, userId]);
// For the message preview we fake the sender as ourselves
const mockEvent = new MatrixEvent({
type: "m.room.message",
sender: userId,
content: event.getContent(),
unsigned: {
age: 97,
},
event_id: "$9999999999999999999999999999999999999999999",
room_id: event.getRoomId(),
});
mockEvent.sender = {
name: profileInfo.displayname || userId,
userId,
getAvatarUrl: (..._) => {
return avatarUrlForUser(
{ avatarUrl: profileInfo.avatar_url },
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => profileInfo.avatar_url,
};
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
const spacesEnabled = useFeatureEnabled("feature_spaces");
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
const previewLayout = useSettingValue<Layout>("layout");
let rooms = useMemo(() => sortRooms(
cli.getVisibleRooms().filter(
room => room.getMyMembership() === "join" &&
!(spacesEnabled && room.isSpaceRoom()),
),
), [cli, spacesEnabled]);
if (lcQuery) {
rooms = new QueryMatcher<Room>(rooms, {
keys: ["name"],
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
shouldMatchWordsOnly: false,
}).match(lcQuery);
}
return <BaseDialog
title={_t("Forward message")}
className="mx_ForwardDialog"
contentId="mx_ForwardList"
onFinished={onFinished}
fixedWidth={false}
>
<h3>{ _t("Message preview") }</h3>
<div className={classnames("mx_ForwardDialog_preview", {
"mx_IRCLayout": previewLayout == Layout.IRC,
"mx_GroupLayout": previewLayout == Layout.Group,
})}>
<EventTile
mxEvent={mockEvent}
layout={previewLayout}
enableFlair={flairEnabled}
permalinkCreator={permalinkCreator}
as="div"
/>
</div>
<hr />
<div className="mx_ForwardList" id="mx_ForwardList">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search for rooms or people")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ForwardList_content">
{ rooms.length > 0 ? (
<div className="mx_ForwardList_results">
{ rooms.map(room =>
<Entry
key={room.roomId}
room={room}
event={event}
matrixClient={cli}
onFinished={onFinished}
/>,
) }
</div>
) : <span className="mx_ForwardList_noResults">
{ _t("No results") }
</span> }
</AutoHideScrollbar>
</div>
</BaseDialog>;
};
export default ForwardDialog;

View file

@ -15,5 +15,5 @@ limitations under the License.
*/
export interface IDialogProps {
onFinished: (bool) => void;
onFinished(...args: any): void;
}

View file

@ -159,7 +159,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
stickyBottom={false}
startAtBottom={false}
>
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
<ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
</ScrollPanel>);
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -19,7 +19,7 @@ import React from 'react';
import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip';
import Tooltip, {Alignment} from './Tooltip';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
tooltipClassName?: string;
forceHide?: boolean;
yOffset?: number;
alignment?: Alignment;
}
interface IState {
@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title}
yOffset={yOffset}
alignment={alignment}
/> : null;
return (
<AccessibleButton

View file

@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
// The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id);
this._sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady);
try {
this._sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady);
} catch (e) {
console.log("Failed to construct widget", e);
this._sgWidget = null;
}
this.iframe = null; // ref to the iframe (callback style)
this.state = this._getNewState(props);
@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
// Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey);
this._sgWidget.stop();
if (this._sgWidget) this._sgWidget.stop();
}
this.setState({ hasPermissionToLoad });
@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
componentDidMount() {
// Only fetch IM token on mount if we're showing and have permission to load
if (this.state.hasPermissionToLoad) {
if (this._sgWidget && this.state.hasPermissionToLoad) {
this._startWidget();
}
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
if (this._sgWidget) {
this._sgWidget.stop();
}
this._sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady);
this._startWidget();
try {
this._sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady);
this._startWidget();
} catch (e) {
console.log("Failed to construct widget", e);
this._sgWidget = null;
}
}
_startWidget() {
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
_iframeRefChange = (ref) => {
this.iframe = ref;
if (ref) {
this._sgWidget.start(ref);
if (this._sgWidget) this._sgWidget.start(ref);
} else {
this._resetWidget(this.props);
}
@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
// Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey);
this._sgWidget.stop({forceDestroy: true});
if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
}
_onWidgetPrepared = () => {
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
<Spinner message={_t("Loading...")} />
</div>
);
if (!this.state.hasPermissionToLoad) {
if (this._sgWidget === null) {
appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg={_t("Error loading Widget")} />
</div>
);
} else if (!this.state.hasPermissionToLoad) {
// only possible for room widgets, can assert this.props.room here
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
if (this.isMixedContent()) {
appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg="Error - Mixed content" />
<AppWarning errorMsg={_t("Error - Mixed content")} />
</div>
);
} else {
@ -417,6 +433,8 @@ export default class AppTile extends React.Component {
onFinished={this._closeContextMenu}
showUnpin={!this.props.userWidget}
userWidget={this.props.userWidget}
onEditClick={this.props.onEditClick}
onDeleteClick={this.props.onDeleteClick}
/>
);
}

View file

@ -63,9 +63,9 @@ const EventListSummary: React.FC<IProps> = ({
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
{ children }
</div>
</li>
);
}

View file

@ -19,20 +19,20 @@ limitations under the License.
import React, { createRef } from 'react';
import { _t } from '../../../languageHandler';
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import {Key} from "../../../Keyboard";
import { Key } from "../../../Keyboard";
import FocusLock from "react-focus-lock";
import MemberAvatar from "../avatars/MemberAvatar";
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import MessageContextMenu from "../context_menus/MessageContextMenu";
import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu';
import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu';
import MessageTimestamp from "../messages/MessageTimestamp";
import SettingsStore from "../../../settings/SettingsStore";
import {formatFullDate} from "../../../DateUtils";
import { formatFullDate } from "../../../DateUtils";
import dis from '../../../dispatcher/dispatcher';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {normalizeWheelEvent} from "../../../utils/Mouse";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { normalizeWheelEvent } from "../../../utils/Mouse";
// Max scale to keep gaps around the image
const MAX_SCALE = 0.95;
@ -95,8 +95,6 @@ export default class ImageView extends React.Component<IProps, IState> {
private initX = 0;
private initY = 0;
private lastX = 0;
private lastY = 0;
private previousX = 0;
private previousY = 0;
@ -105,23 +103,35 @@ export default class ImageView extends React.Component<IProps, IState> {
// needs to be passive in order to work with Chromium
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
// We want to recalculate zoom whenever the window's size changes
window.addEventListener("resize", this.calculateZoom);
window.addEventListener("resize", this.recalculateZoom);
// After the image loads for the first time we want to calculate the zoom
this.image.current.addEventListener("load", this.calculateZoom);
this.image.current.addEventListener("load", this.recalculateZoom);
}
componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel);
window.removeEventListener("resize", this.calculateZoom);
this.image.current.removeEventListener("load", this.calculateZoom);
window.removeEventListener("resize", this.recalculateZoom);
this.image.current.removeEventListener("load", this.recalculateZoom);
}
private calculateZoom = () => {
private recalculateZoom = () => {
this.setZoomAndRotation();
}
private setZoomAndRotation = (inputRotation?: number) => {
const image = this.image.current;
const imageWrapper = this.imageWrapper.current;
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
const rotation = inputRotation || this.state.rotation;
const imageIsNotFlipped = rotation % 180 === 0;
// If the image is rotated take it into account
const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight;
const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth;
const zoomX = imageWrapper.clientWidth / width;
const zoomY = imageWrapper.clientHeight / height;
// If the image is smaller in both dimensions set its the zoom to 1 to
// display it in its original size
@ -130,6 +140,7 @@ export default class ImageView extends React.Component<IProps, IState> {
zoom: 1,
minZoom: 1,
maxZoom: 1,
rotation: rotation,
});
return;
}
@ -138,10 +149,14 @@ export default class ImageView extends React.Component<IProps, IState> {
// image by default
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
// If zoom is smaller than minZoom don't go below that value
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
this.setState({
minZoom: minZoom,
maxZoom: 1,
rotation: rotation,
zoom: zoom,
});
}
@ -157,7 +172,7 @@ export default class ImageView extends React.Component<IProps, IState> {
return;
}
if (newZoom >= this.state.maxZoom) {
this.setState({zoom: this.state.maxZoom});
this.setState({ zoom: this.state.maxZoom });
return;
}
@ -170,7 +185,7 @@ export default class ImageView extends React.Component<IProps, IState> {
ev.stopPropagation();
ev.preventDefault();
const {deltaY} = normalizeWheelEvent(ev);
const { deltaY } = normalizeWheelEvent(ev);
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
};
@ -192,14 +207,12 @@ export default class ImageView extends React.Component<IProps, IState> {
private onRotateCounterClockwiseClick = () => {
const cur = this.state.rotation;
const rotationDegrees = cur - 90;
this.setState({ rotation: rotationDegrees });
this.setZoomAndRotation(cur - 90);
};
private onRotateClockwiseClick = () => {
const cur = this.state.rotation;
const rotationDegrees = cur + 90;
this.setState({ rotation: rotationDegrees });
this.setZoomAndRotation(cur + 90);
};
private onDownloadClick = () => {
@ -246,15 +259,15 @@ export default class ImageView extends React.Component<IProps, IState> {
// Zoom in if we are completely zoomed out
if (this.state.zoom === this.state.minZoom) {
this.setState({zoom: this.state.maxZoom});
this.setState({ zoom: this.state.maxZoom });
return;
}
this.setState({moving: true});
this.setState({ moving: true });
this.previousX = this.state.translationX;
this.previousY = this.state.translationY;
this.initX = ev.pageX - this.lastX;
this.initY = ev.pageY - this.lastY;
this.initX = ev.pageX - this.state.translationX;
this.initY = ev.pageY - this.state.translationY;
};
private onMoving = (ev: React.MouseEvent) => {
@ -263,11 +276,9 @@ export default class ImageView extends React.Component<IProps, IState> {
if (!this.state.moving) return;
this.lastX = ev.pageX - this.initX;
this.lastY = ev.pageY - this.initY;
this.setState({
translationX: this.lastX,
translationY: this.lastY,
translationX: ev.pageX - this.initX,
translationY: ev.pageY - this.initY,
});
};
@ -283,8 +294,10 @@ export default class ImageView extends React.Component<IProps, IState> {
translationX: 0,
translationY: 0,
});
this.initX = 0;
this.initY = 0;
}
this.setState({moving: false});
this.setState({ moving: false });
};
private renderContextMenu() {
@ -355,7 +368,7 @@ export default class ImageView extends React.Component<IProps, IState> {
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
const sender = (
<div className="mx_ImageView_info_sender">
{senderName}
{ senderName }
</div>
);
const messageTimestamp = (
@ -382,10 +395,10 @@ export default class ImageView extends React.Component<IProps, IState> {
info = (
<div className="mx_ImageView_info_wrapper">
{avatar}
{ avatar }
<div className="mx_ImageView_info">
{sender}
{messageTimestamp}
{ sender }
{ messageTimestamp }
</div>
</div>
);
@ -425,7 +438,7 @@ export default class ImageView extends React.Component<IProps, IState> {
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomIn"
title={_t("Zoom in")}
onClick={ this.onZoomInClick }>
onClick={this.onZoomInClick}>
</AccessibleTooltipButton>
);
}
@ -441,7 +454,7 @@ export default class ImageView extends React.Component<IProps, IState> {
ref={this.focusLock}
>
<div className="mx_ImageView_panel">
{info}
{ info }
<div className="mx_ImageView_toolbar">
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
@ -453,25 +466,30 @@ export default class ImageView extends React.Component<IProps, IState> {
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
{zoomOutButton}
{zoomInButton}
{ zoomOutButton }
{ zoomInButton }
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_download"
title={_t("Download")}
onClick={ this.onDownloadClick }>
</AccessibleTooltipButton>
{contextMenuButton}
{ contextMenuButton }
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_close"
title={_t("Close")}
onClick={ this.props.onFinished }>
</AccessibleTooltipButton>
{this.renderContextMenu()}
{ this.renderContextMenu() }
</div>
</div>
<div
className="mx_ImageView_image_wrapper"
ref={this.imageWrapper}>
ref={this.imageWrapper}
onMouseDown={this.props.onFinished}
onMouseMove={this.onMoving}
onMouseUp={this.onEndMoving}
onMouseLeave={this.onEndMoving}
>
<img
src={this.props.src}
title={this.props.name}
@ -480,9 +498,6 @@ export default class ImageView extends React.Component<IProps, IState> {
className="mx_ImageView_image"
draggable={true}
onMouseDown={this.onStartMoving}
onMouseMove={this.onMoving}
onMouseUp={this.onEndMoving}
onMouseLeave={this.onEndMoving}
/>
</div>
</FocusLock>

View file

@ -46,6 +46,8 @@ export default class ReplyThread extends React.Component {
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
// Specifies which layout to use.
layout: LayoutPropType,
// Whether to always show a timestamp
alwaysShowTimestamps: PropTypes.bool,
};
static contextType = MatrixClientContext;
@ -212,7 +214,7 @@ export default class ReplyThread extends React.Component {
};
}
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) {
if (!ReplyThread.getParentEventId(parentEv)) {
return null;
}
@ -222,6 +224,7 @@ export default class ReplyThread extends React.Component {
ref={ref}
permalinkCreator={permalinkCreator}
layout={layout}
alwaysShowTimestamps={alwaysShowTimestamps}
/>;
}
@ -386,6 +389,7 @@ export default class ReplyThread extends React.Component {
isRedacted={ev.isRedacted()}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
replacingEventId={ev.replacingEventId()}
/>

View file

@ -21,12 +21,12 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HeaderKind} from './HeaderButtons';
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import HeaderButtons, { HeaderKind } from './HeaderButtons';
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { Action } from "../../../dispatcher/actions";
import { ActionPayload } from "../../../dispatcher/payloads";
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const GROUP_PHASES = [
RightPanelPhases.GroupMemberInfo,
@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons {
};
renderButtons() {
return [
<HeaderButton key="groupMembersButton" name="groupMembersButton"
return <>
<HeaderButton
name="groupMembersButton"
title={_t('Members')}
isHighlighted={this.isPhase(GROUP_PHASES)}
onClick={this.onMembersClicked}
analytics={['Right Panel', 'Group Member List Button', 'click']}
/>,
<HeaderButton key="roomsButton" name="roomsButton"
/>
<HeaderButton
name="roomsButton"
title={_t('Rooms')}
isHighlighted={this.isPhase(ROOM_PHASES)}
onClick={this.onRoomsClicked}
analytics={['Right Panel', 'Group Room List Button', 'click']}
/>,
];
/>
</>;
}
}

View file

@ -22,15 +22,13 @@ import React from 'react';
import classNames from 'classnames';
import Analytics from '../../../Analytics';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// Whether this button is highlighted
isHighlighted: boolean;
// click handler
onClick: () => void;
// The badge to display above the icon
badge?: React.ReactNode;
// The parameters to track the click event
analytics: Parameters<typeof Analytics.trackEvent>;
@ -40,31 +38,29 @@ interface IProps {
title: string;
}
// TODO: replace this, the composer buttons and the right panel buttons with a unified
// representation
// TODO: replace this, the composer buttons and the right panel buttons with a unified representation
@replaceableComponent("views.right_panel.HeaderButton")
export default class HeaderButton extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
this.onClick = this.onClick.bind(this);
}
private onClick() {
private onClick = () => {
Analytics.trackEvent(...this.props.analytics);
this.props.onClick();
}
};
public render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {isHighlighted, onClick, analytics, name, title, ...props} = this.props;
const classes = classNames({
mx_RightPanel_headerButton: true,
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
[`mx_RightPanel_${this.props.name}`]: true,
mx_RightPanel_headerButton_highlight: isHighlighted,
[`mx_RightPanel_${name}`]: true,
});
return <AccessibleTooltipButton
aria-selected={this.props.isHighlighted}
{...props}
aria-selected={isHighlighted}
role="tab"
title={this.props.title}
title={title}
className={classes}
onClick={this.onClick}
/>;

View file

@ -21,14 +21,14 @@ limitations under the License.
import React from 'react';
import dis from '../../../dispatcher/dispatcher';
import RightPanelStore from "../../../stores/RightPanelStore";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from '../../../dispatcher/actions';
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { Action } from '../../../dispatcher/actions';
import {
SetRightPanelPhasePayload,
SetRightPanelPhaseRefireParams,
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
import {EventSubscription} from "fbemitter";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import type { EventSubscription } from "fbemitter";
import { replaceableComponent } from "../../../utils/replaceableComponent";
export enum HeaderKind {
Room = "room",
@ -43,11 +43,11 @@ interface IState {
interface IProps {}
@replaceableComponent("views.right_panel.HeaderButtons")
export default abstract class HeaderButtons extends React.Component<IProps, IState> {
export default abstract class HeaderButtons<P = {}> extends React.Component<IProps & P, IState> {
private storeToken: EventSubscription;
private dispatcherRef: string;
constructor(props: IProps, kind: HeaderKind) {
constructor(props: IProps & P, kind: HeaderKind) {
super(props);
const rps = RightPanelStore.getSharedInstance();
@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
}
// XXX: Make renderButtons a prop
public abstract renderButtons(): JSX.Element[];
public abstract renderButtons(): JSX.Element;
public render() {
return <div className="mx_HeaderButtons">

View file

@ -0,0 +1,188 @@
/*
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.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useCallback, useContext, useEffect, useState} from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { _t } from "../../../languageHandler";
import BaseCard from "./BaseCard";
import Spinner from "../elements/Spinner";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import PinningUtils from "../../../utils/PinningUtils";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import PinnedEventTile from "../rooms/PinnedEventTile";
interface IProps {
room: Room;
onClose(): void;
}
export const usePinnedEvents = (room: Room): string[] => {
const [pinnedEvents, setPinnedEvents] = useState<string[]>([]);
const update = useCallback((ev?: MatrixEvent) => {
if (!room) return;
if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []);
}, [room]);
useEventEmitter(room?.currentState, "RoomState.events", update);
useEffect(() => {
update();
return () => {
setPinnedEvents([]);
};
}, [update]);
return pinnedEvents;
};
export const ReadPinsEventId = "im.vector.room.read_pins";
export const useReadPinnedEvents = (room: Room): Set<string> => {
const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set());
const update = useCallback((ev?: MatrixEvent) => {
if (!room) return;
if (ev && ev.getType() !== ReadPinsEventId) return;
const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids;
setReadPinnedEvents(new Set(readPins || []));
}, [room]);
useEventEmitter(room, "Room.accountData", update);
useEffect(() => {
update();
return () => {
setReadPinnedEvents(new Set());
};
}, [update]);
return readPinnedEvents;
};
const useRoomState = <T extends any>(room: Room, mapper: (state: RoomState) => T): T => {
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
const update = useCallback(() => {
if (!room) return;
setValue(mapper(room.currentState));
}, [room, mapper]);
useEventEmitter(room?.currentState, "RoomState.events", update);
useEffect(() => {
update();
return () => {
setValue(undefined);
};
}, [update]);
return value;
};
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
const cli = useContext(MatrixClientContext);
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
const pinnedEventIds = usePinnedEvents(room);
const readPinnedEvents = useReadPinnedEvents(room);
useEffect(() => {
const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id));
if (newlyRead.length > 0) {
// clear out any read pinned events which no longer are pinned
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
event_ids: pinnedEventIds,
});
}
}, [cli, room.roomId, pinnedEventIds, readPinnedEvents]);
const pinnedEvents = useAsyncMemo(() => {
const promises = pinnedEventIds.map(async eventId => {
const timelineSet = room.getUnfilteredTimelineSet();
const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId);
if (localEvent) return localEvent;
try {
const evJson = await cli.fetchRoomEvent(room.roomId, eventId);
const event = new MatrixEvent(evJson);
if (event.isEncrypted()) {
await cli.decryptEventIfNeeded(event); // TODO await?
}
if (event && PinningUtils.isPinnable(event)) {
return event;
}
} catch (err) {
console.error("Error looking up pinned event " + eventId + " in room " + room.roomId);
console.error(err);
}
return null;
});
return Promise.all(promises);
}, [cli, room, pinnedEventIds], null);
let content;
if (!pinnedEvents) {
content = <Spinner />;
} else if (pinnedEvents.length > 0) {
let onUnpinClicked;
if (canUnpin) {
onUnpinClicked = async (event: MatrixEvent) => {
const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
if (pinnedEvents?.getContent()?.pinned) {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(event.getId());
if (index !== -1) {
pinned.splice(index, 1);
await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, "");
}
}
};
}
// show them in reverse, with latest pinned at the top
content = pinnedEvents.filter(Boolean).reverse().map(ev => (
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
));
} else {
content = <div className="mx_PinnedMessagesCard_empty">
<div>
{ /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
<div className="mx_PinnedMessagesCard_MessageActionBar">
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" />
</div>
<h2>{ _t("Nothing pinned, yet") }</h2>
{ _t("If you have permissions, open the menu on any message and select " +
"<b>Pin</b> to stick them here.", {}, {
b: sub => <b>{ sub }</b>,
}) }
</div>
</div>;
}
return <BaseCard
header={<h2>{ _t("Pinned messages") }</h2>}
className="mx_PinnedMessagesCard"
onClose={onClose}
>
{ content }
</BaseCard>;
};
export default PinnedMessagesCard;

View file

@ -18,15 +18,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import {_t} from '../../../languageHandler';
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HeaderKind} from './HeaderButtons';
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
import HeaderButtons, { HeaderKind } from './HeaderButtons';
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { Action } from "../../../dispatcher/actions";
import { ActionPayload } from "../../../dispatcher/payloads";
import RightPanelStore from "../../../stores/RightPanelStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { useSettingValue } from "../../../hooks/useSettings";
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary,
@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
RightPanelPhases.Room3pidMemberInfo,
];
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
const pinningEnabled = useSettingValue("feature_pinning");
const pinnedEvents = usePinnedEvents(pinningEnabled && room);
const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
if (!pinningEnabled) return null;
let unreadIndicator;
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
unreadIndicator = <div className="mx_RightPanel_pinnedMessagesButton_unreadIndicator" />;
}
return <HeaderButton
name="pinnedMessagesButton"
title={_t("Pinned messages")}
isHighlighted={isHighlighted}
onClick={onClick}
analytics={["Right Panel", "Pinned Messages Button", "click"]}
>
{ unreadIndicator }
</HeaderButton>;
};
interface IProps {
room?: Room;
}
@replaceableComponent("views.right_panel.RoomHeaderButtons")
export default class RoomHeaderButtons extends HeaderButtons {
constructor(props) {
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
constructor(props: IProps) {
super(props, HeaderKind.Room);
}
@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons {
this.setPhase(RightPanelPhases.NotificationPanel);
};
private onPinnedMessagesClicked = () => {
// This toggles for us, if needed
this.setPhase(RightPanelPhases.PinnedMessages);
};
public renderButtons() {
return [
return <>
<PinnedMessagesHeaderButton
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
onClick={this.onPinnedMessagesClicked}
/>
<HeaderButton
key="notifsButton"
name="notifsButton"
title={_t('Notifications')}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
onClick={this.onNotificationsClicked}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,
/>
<HeaderButton
key="roomSummaryButton"
name="roomSummaryButton"
title={_t('Room Info')}
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
onClick={this.onRoomSummaryClicked}
analytics={['Right Panel', 'Room Summary Button', 'click']}
/>,
];
/>
</>;
}
}

View file

@ -17,18 +17,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
import {MatrixClient} from 'matrix-js-sdk/src/client';
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
import {User} from 'matrix-js-sdk/src/models/user';
import {Room} from 'matrix-js-sdk/src/models/room';
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { User } from 'matrix-js-sdk/src/models/user';
import { Room } from 'matrix-js-sdk/src/models/room';
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import {_t} from '../../../languageHandler';
import { _t } from '../../../languageHandler';
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
@ -39,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter";
import GroupStore from "../../../stores/GroupStore";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {textualPowerLevel} from '../../../Roles';
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { textualPowerLevel } from '../../../Roles';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
import {Action} from "../../../dispatcher/actions";
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
import { Action } from "../../../dispatcher/actions";
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
import {E2EStatus} from "../../../utils/ShieldUtils";
import { E2EStatus } from "../../../utils/ShieldUtils";
import ImageView from "../elements/ImageView";
import Spinner from "../elements/Spinner";
import PowerSelector from "../elements/PowerSelector";
@ -65,7 +66,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import {mediaFromMxc} from "../../../customisations/Media";
import { mediaFromMxc } from "../../../customisations/Media";
import UIStore from "../../../stores/UIStore";
export interface IDevice {
@ -514,9 +515,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
} else {
setPowerLevels({});
}
return () => {
setPowerLevels({});
};
}, [room]);
useEventEmitter(cli, "RoomState.events", update);
@ -1530,21 +1528,16 @@ interface IProps {
user: Member;
groupId?: string;
room?: Room;
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
phase: RightPanelPhases.RoomMemberInfo
| RightPanelPhases.GroupMemberInfo
| RightPanelPhases.SpaceMemberInfo
| RightPanelPhases.EncryptionPanel;
onClose(): void;
verificationRequest?: VerificationRequest;
verificationRequestPromise?: Promise<VerificationRequest>;
}
interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> {
user: Member;
groupId: void;
room: Room;
phase: RightPanelPhases.EncryptionPanel;
onClose(): void;
}
type Props = IProps | IPropsWithEncryptionPanel;
const UserInfo: React.FC<Props> = ({
const UserInfo: React.FC<IProps> = ({
user,
groupId,
room,

View file

@ -644,7 +644,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars" />);
return null;
}
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
@ -906,6 +906,12 @@ export default class EventTile extends React.Component<IProps, IState> {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
}
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
const scrollToken = this.props.mxEvent.status
? undefined
: this.props.mxEvent.getId();
let avatar;
let sender;
let avatarSize;
@ -975,7 +981,8 @@ export default class EventTile extends React.Component<IProps, IState> {
onFocusChange={this.onActionBarFocusChange}
/> : undefined;
const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover);
const showTimestamp = this.props.mxEvent.getTs() &&
(this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused);
const timestamp = showTimestamp ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -1046,7 +1053,7 @@ export default class EventTile extends React.Component<IProps, IState> {
case 'notif': {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
<div className="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} />
<a href={permalink} onClick={this.onPermalinkClicked}>
@ -1069,12 +1076,12 @@ export default class EventTile extends React.Component<IProps, IState> {
onHeightChanged={this.props.onHeightChanged}
/>
</div>
</div>
</li>
);
}
case 'file_grid': {
return (
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
<div className="mx_EventTile_line">
<EventTileType ref={this.tile}
mxEvent={this.props.mxEvent}
@ -1095,7 +1102,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ timestamp }
</div>
</a>
</div>
</li>
);
}
@ -1108,10 +1115,12 @@ export default class EventTile extends React.Component<IProps, IState> {
this.props.onHeightChanged,
this.props.permalinkCreator,
this.replyThread,
null,
this.props.alwaysShowTimestamps || this.state.hover,
);
}
return (
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
{ ircTimestamp }
{ avatar }
{ sender }
@ -1129,7 +1138,7 @@ export default class EventTile extends React.Component<IProps, IState> {
showUrlPreview={false}
/>
</div>
</div>
</li>
);
}
default: {
@ -1139,17 +1148,18 @@ export default class EventTile extends React.Component<IProps, IState> {
this.props.permalinkCreator,
this.replyThread,
this.props.layout,
this.props.alwaysShowTimestamps || this.state.hover,
);
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
React.createElement(this.props.as || "div", {
React.createElement(this.props.as || "li", {
"ref": this.ref,
"className": classes,
"tabIndex": -1,
"aria-live": ariaLive,
"aria-atomic": "true",
"data-scroll-tokens": this.props["data-scroll-tokens"],
"data-scroll-tokens": scrollToken,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, [
@ -1340,11 +1350,15 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
}
return <span className="mx_EventTile_readAvatars">
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
{nonCssBadge}
{tooltip}
</span>
</span>;
return (
<div className="mx_EventTile_msgOption">
<span className="mx_EventTile_readAvatars">
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
{nonCssBadge}
{tooltip}
</span>
</span>
</div>
);
}
}

View file

@ -1,53 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 Michael Telatynski
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.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {Key} from '../../../Keyboard';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.ForwardMessage")
export default class ForwardMessage extends React.Component {
static propTypes = {
onCancelClick: PropTypes.func.isRequired,
};
componentDidMount() {
document.addEventListener('keydown', this._onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this._onKeyDown);
}
_onKeyDown = ev => {
switch (ev.key) {
case Key.ESCAPE:
this.props.onCancelClick();
break;
}
};
render() {
return (
<div className="mx_ForwardMessage">
<h1>{ _t('Please select the destination room for this message') }</h1>
</div>
);
}
}

View file

@ -1,111 +0,0 @@
/*
Copyright 2017 Travis Ralston
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.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import {formatFullDate} from '../../../DateUtils';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.PinnedEventTile")
export default class PinnedEventTile extends React.Component {
static propTypes = {
mxRoom: PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
onUnpinned: PropTypes.func,
};
onTileClicked = () => {
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
};
onUnpinClicked = () => {
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
// Nothing to do: already unpinned
if (this.props.onUnpinned) this.props.onUnpinned();
} else {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(this.props.mxEvent.getId());
if (index !== -1) {
pinned.splice(index, 1);
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
.then(() => {
if (this.props.onUnpinned) this.props.onUnpinned();
});
} else if (this.props.onUnpinned) this.props.onUnpinned();
}
};
_canUnpin() {
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
}
render() {
const sender = this.props.mxEvent.getSender();
// Get the latest sender profile rather than historical
const senderProfile = this.props.mxRoom.getMember(sender);
const avatarSize = 40;
let unpinButton = null;
if (this._canUnpin()) {
unpinButton = (
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
<img src={require("../../../../res/img/cancel-red.svg")} width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
</AccessibleButton>
);
}
return (
<div className="mx_PinnedEventTile">
<div className="mx_PinnedEventTile_actions">
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
{ _t("Jump to message") }
</AccessibleButton>
{ unpinButton }
</div>
<span className="mx_PinnedEventTile_senderAvatar">
<MemberAvatar member={senderProfile} width={avatarSize} height={avatarSize} fallbackUserId={sender} />
</span>
<span className="mx_PinnedEventTile_sender">
{ senderProfile ? senderProfile.name : sender }
</span>
<span className="mx_PinnedEventTile_timestamp">
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
</span>
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.mxEvent}
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
/>
</div>
</div>
);
}
}

View file

@ -0,0 +1,104 @@
/*
Copyright 2017 Travis Ralston
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.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import { formatDate } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps {
room: Room;
event: MatrixEvent;
onUnpinClicked?(): void;
}
const AVATAR_SIZE = 24;
@replaceableComponent("views.rooms.PinnedEventTile")
export default class PinnedEventTile extends React.Component<IProps> {
public static contextType = MatrixClientContext;
private onTileClicked = () => {
dis.dispatch({
action: 'view_room',
event_id: this.props.event.getId(),
highlighted: true,
room_id: this.props.event.getRoomId(),
});
};
render() {
const sender = this.props.event.getSender();
const senderProfile = this.props.room.getMember(sender);
let unpinButton = null;
if (this.props.onUnpinClicked) {
unpinButton = (
<AccessibleTooltipButton
onClick={this.props.onUnpinClicked}
className="mx_PinnedEventTile_unpinButton"
title={_t("Unpin")}
/>
);
}
return <div className="mx_PinnedEventTile">
<MemberAvatar
className="mx_PinnedEventTile_senderAvatar"
member={senderProfile}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
fallbackUserId={sender}
/>
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
{ senderProfile?.name || sender }
</span>
{ unpinButton }
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.event}
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
/>
</div>
<div className="mx_PinnedEventTile_footer">
<span className="mx_PinnedEventTile_timestamp">
{ formatDate(new Date(this.props.event.getTs())) }
</span>
<AccessibleButton onClick={this.onTileClicked} kind="link">
{ _t("View message") }
</AccessibleButton>
</div>
</div>;
}
}

View file

@ -1,145 +0,0 @@
/*
Copyright 2017 Travis Ralston
Copyright 2019 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.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
import PinningUtils from "../../../utils/PinningUtils";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.PinnedEventsPanel")
export default class PinnedEventsPanel extends React.Component {
static propTypes = {
// The Room from the js-sdk we're going to show pinned events for
room: PropTypes.object.isRequired,
onCancelClick: PropTypes.func,
};
state = {
loading: true,
};
componentDidMount() {
this._updatePinnedMessages();
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
}
}
_onStateEvent = ev => {
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
this._updatePinnedMessages();
}
};
_updatePinnedMessages = () => {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
this.setState({ loading: false, pinned: [] });
} else {
const promises = [];
const cli = MatrixClientPeg.get();
pinnedEvents.getContent().pinned.map((eventId) => {
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
(timeline) => {
const event = timeline.getEvents().find((e) => e.getId() === eventId);
return {eventId, timeline, event};
}).catch((err) => {
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
console.error(err);
return null; // return lack of context to avoid unhandled errors
}));
});
Promise.all(promises).then((contexts) => {
// Filter out the messages before we try to render them
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
this.setState({ loading: false, pinned });
});
}
this._updateReadState();
};
_updateReadState() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents) return; // nothing to read
let readStateEvents = [];
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
readStateEvents = readPinsEvent.getContent().event_ids || [];
}
if (!readStateEvents.includes(pinnedEvents.getId())) {
readStateEvents.push(pinnedEvents.getId());
// Only keep the last 10 event IDs to avoid infinite growth
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
event_ids: readStateEvents,
});
}
}
_getPinnedTiles() {
if (this.state.pinned.length === 0) {
return (<div>{ _t("No pinned messages.") }</div>);
}
return this.state.pinned.map((context) => {
return (
<PinnedEventTile
key={context.event.getId()}
mxRoom={this.props.room}
mxEvent={context.event}
onUnpinned={this._updatePinnedMessages}
/>
);
});
}
render() {
let tiles = <div>{ _t("Loading...") }</div>;
if (this.state && !this.state.loading) {
tiles = this._getPinnedTiles();
}
return (
<div className="mx_PinnedEventsPanel">
<div className="mx_PinnedEventsPanel_body">
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
</AccessibleButton>
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
{ tiles }
</div>
</div>
);
}
}

View file

@ -89,7 +89,7 @@ export default class ReplyPreview extends React.Component {
</div>
<div className="mx_ReplyPreview_clear" />
<EventTile
last={true}
alwaysShowTimestamps={true}
tileShape="reply_preview"
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}

View file

@ -19,10 +19,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RateLimitedFunc from '../../../ratelimitedfunc';
import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon';
@ -30,8 +29,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import {PlaceCallType} from "../../../CallHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { PlaceCallType } from "../../../CallHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.RoomHeader")
export default class RoomHeader extends React.Component {
@ -40,10 +39,8 @@ export default class RoomHeader extends React.Component {
oobData: PropTypes.object,
inRoom: PropTypes.bool,
onSettingsClick: PropTypes.func,
onPinnedClick: PropTypes.func,
onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func,
appsShown: PropTypes.bool,
@ -53,20 +50,17 @@ export default class RoomHeader extends React.Component {
static defaultProps = {
editing: false,
inRoom: false,
onCancelClick: null,
};
componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
cli.on("Room.accountData", this._onRoomAccountData);
}
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
cli.removeListener("Room.accountData", this._onRoomAccountData);
}
}
@ -79,52 +73,13 @@ export default class RoomHeader extends React.Component {
this._rateLimitedUpdate();
};
_onRoomAccountData = (event, room) => {
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
if (event.getType() !== "im.vector.room.read_pins") return;
this._rateLimitedUpdate();
};
_rateLimitedUpdate = new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
}, 500);
_hasUnreadPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
return false; // no pins == nothing to read
}
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
const readStateEvents = readPinsEvent.getContent().event_ids || [];
if (readStateEvents) {
return !readStateEvents.includes(currentPinEvent.getId());
}
}
// There's pins, and we haven't read any of them
return true;
}
_hasPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
}
render() {
let searchStatus = null;
let cancelButton = null;
let pinnedEventsButton = null;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
// don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount.
@ -181,24 +136,6 @@ export default class RoomHeader extends React.Component {
/>;
}
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
} else if (this._hasPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
}
pinnedEventsButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
onClick={this.props.onPinnedClick}
title={_t("Pinned Messages")}
>
{ pinsIndicator }
</AccessibleTooltipButton>;
}
let forgetButton;
if (this.props.onForgetClick) {
forgetButton =
@ -248,7 +185,6 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_buttons">
{ videoCallButton }
{ voiceCallButton }
{ pinnedEventsButton }
{ forgetButton }
{ appsButton }
{ searchButton }
@ -263,9 +199,8 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ name }
{ topicElement }
{ cancelButton }
{ rightRow }
<RoomHeaderButtons />
<RoomHeaderButtons room={this.props.room} />
</div>
</div>
);

View file

@ -47,6 +47,7 @@ export default class SearchResultTile extends React.Component {
const ts1 = mxEv.getTs();
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
const timeline = result.context.getTimeline();
for (let j = 0; j < timeline.length; j++) {
@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component {
highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
alwaysShowTimestamps={alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
));

View file

@ -16,23 +16,9 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from '../elements/AccessibleButton';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
const {onClick} = props;
return (
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>
);
}
/*
* A stripped-down room header used for things like the user settings
* and room directory.
@ -41,18 +27,13 @@ export function CancelButton(props) {
export default class SimpleRoomHeader extends React.Component {
static propTypes = {
title: PropTypes.string,
onCancelClick: PropTypes.func,
// `src` to a TintableSvg. Optional.
icon: PropTypes.string,
};
render() {
let cancelButton;
let icon;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg
@ -66,7 +47,6 @@ export default class SimpleRoomHeader extends React.Component {
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ cancelButton }
</div>
</div>
);

View file

@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent {
/**
* Launch the integration manager on the stickers integration page
*/
_launchManageIntegrations() {
_launchManageIntegrations = () => {
// TODO: Open the right integration manager for the widget
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent {
this.state.widgetId,
);
}
}
};
render() {
let stickerPicker;
@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent {
key="controls_hide_stickers"
className={className}
onClick={this._onHideStickersClick}
active={this.state.showStickers}
active={this.state.showStickers.toString()}
title={_t("Hide Stickers")}
>
</AccessibleButton>;

View file

@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
async _getUpdatedStatus() {
const cli = MatrixClientPeg.get();
const pkCache = cli.getCrossSigningCacheCallbacks();
const crossSigning = cli._crypto._crossSigningInfo;
const secretStorage = cli._crypto._secretStorage;
const crossSigning = cli.crypto._crossSigningInfo;
const secretStorage = cli.crypto._secretStorage;
const crossSigningPublicKeysOnDevice = crossSigning.getId();
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));

View file

@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent {
async _getUpdatedDiagnostics() {
const cli = MatrixClientPeg.get();
const secretStorage = cli._crypto._secretStorage;
const secretStorage = cli.crypto._secretStorage;
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
const backupKeyCached = !!(backupKeyFromCache);
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
const secretStorageKeyInAccount = await secretStorage.hasKey();

View file

@ -15,17 +15,14 @@ limitations under the License.
*/
import * as React from "react";
import { ensureDMExists } from "../../../createRoom";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import DialPad from './DialPad';
import dis from '../../../dispatcher/dispatcher';
import Modal from "../../../Modal";
import ErrorDialog from "../../views/dialogs/ErrorDialog";
import CallHandler from "../../../CallHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
import { Action } from "../../../dispatcher/actions";
interface IProps {
onFinished: (boolean) => void;
@ -67,21 +64,11 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
}
onDialPress = async () => {
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, {
title: _t("Unable to look up phone number"),
description: _t("There was an error looking up the phone number"),
});
}
const userId = results[0].userid;
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
const payload: DialNumberPayload = {
action: Action.DialNumber,
number: this.state.value,
};
dis.dispatch(payload);
this.props.onFinished(true);
}