Merge branch 'develop' into sort-imports
Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
commit
7b94e13a84
642 changed files with 30052 additions and 8035 deletions
|
@ -21,7 +21,6 @@ import { EventEmitter } from 'events';
|
|||
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
||||
export enum CallEventGrouperEvent {
|
||||
StateChanged = "state_changed",
|
||||
|
@ -53,8 +52,8 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall);
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallsChanged, this.setCall);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
}
|
||||
|
||||
private get invite(): MatrixEvent {
|
||||
|
@ -115,7 +114,7 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
}
|
||||
|
||||
private onSilencedCallsChanged = () => {
|
||||
const newState = CallHandler.sharedInstance().isCallSilenced(this.callId);
|
||||
const newState = CallHandler.instance.isCallSilenced(this.callId);
|
||||
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
|
||||
};
|
||||
|
||||
|
@ -123,33 +122,23 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
this.emit(CallEventGrouperEvent.LengthChanged, length);
|
||||
};
|
||||
|
||||
public answerCall = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'answer',
|
||||
room_id: this.roomId,
|
||||
});
|
||||
public answerCall = (): void => {
|
||||
CallHandler.instance.answerCall(this.roomId);
|
||||
};
|
||||
|
||||
public rejectCall = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'reject',
|
||||
room_id: this.roomId,
|
||||
});
|
||||
public rejectCall = (): void => {
|
||||
CallHandler.instance.hangupOrReject(this.roomId, true);
|
||||
};
|
||||
|
||||
public callBack = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'place_call',
|
||||
type: this.isVoice ? CallType.Voice : CallType.Video,
|
||||
room_id: this.roomId,
|
||||
});
|
||||
public callBack = (): void => {
|
||||
CallHandler.instance.placeCall(this.roomId, this.isVoice ? CallType.Voice : CallType.Video);
|
||||
};
|
||||
|
||||
public toggleSilenced = () => {
|
||||
const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId);
|
||||
const silenced = CallHandler.instance.isCallSilenced(this.callId);
|
||||
silenced ?
|
||||
CallHandler.sharedInstance().unSilenceCall(this.callId) :
|
||||
CallHandler.sharedInstance().silenceCall(this.callId);
|
||||
CallHandler.instance.unSilenceCall(this.callId) :
|
||||
CallHandler.instance.silenceCall(this.callId);
|
||||
};
|
||||
|
||||
private setCallListeners() {
|
||||
|
@ -175,7 +164,7 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
private setCall = () => {
|
||||
if (this.call) return;
|
||||
|
||||
this.call = CallHandler.sharedInstance().getCallById(this.callId);
|
||||
this.call = CallHandler.instance.getCallById(this.callId);
|
||||
this.setCallListeners();
|
||||
this.setState();
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ import { Key } from "../../Keyboard";
|
|||
import { Writeable } from "../../@types/common";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import { getInputableElement } from "./LoggedInView";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -49,6 +50,8 @@ export interface IPosition {
|
|||
bottom?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
rightAligned?: boolean;
|
||||
bottomAligned?: boolean;
|
||||
}
|
||||
|
||||
export enum ChevronFace {
|
||||
|
@ -101,7 +104,7 @@ interface IState {
|
|||
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
|
||||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||
@replaceableComponent("structures.ContextMenu")
|
||||
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||
export default class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||
private readonly initialFocus: HTMLElement;
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -246,6 +249,9 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
|
||||
// only handle escape when in an input field
|
||||
if (ev.key !== Key.ESCAPE && getInputableElement(ev.target as HTMLElement)) return;
|
||||
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
|
@ -346,6 +352,8 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
|
||||
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
|
||||
'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom,
|
||||
'mx_ContextualMenu_rightAligned': this.props.rightAligned === true,
|
||||
'mx_ContextualMenu_bottomAligned': this.props.bottomAligned === true,
|
||||
});
|
||||
|
||||
const menuStyle: CSSProperties = {};
|
||||
|
@ -407,6 +415,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
{ background }
|
||||
<div
|
||||
className={menuClasses}
|
||||
style={menuStyle}
|
||||
|
@ -415,7 +424,6 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
>
|
||||
{ body }
|
||||
</div>
|
||||
{ background }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -526,30 +534,22 @@ export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<
|
|||
return [isOpen, button, open, close, setIsOpen];
|
||||
};
|
||||
|
||||
@replaceableComponent("structures.LegacyContextMenu")
|
||||
export default class LegacyContextMenu extends ContextMenu {
|
||||
render() {
|
||||
return this.renderMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs.
|
||||
export function createMenu(ElementClass, props) {
|
||||
const onFinished = function(...args) {
|
||||
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||
|
||||
if (props && props.onFinished) {
|
||||
props.onFinished.apply(null, args);
|
||||
}
|
||||
props?.onFinished?.apply(null, args);
|
||||
};
|
||||
|
||||
const menu = <LegacyContextMenu
|
||||
const menu = <ContextMenu
|
||||
{...props}
|
||||
mountAsChild={true}
|
||||
hasBackground={false}
|
||||
onFinished={onFinished} // eslint-disable-line react/jsx-no-bind
|
||||
windowResize={onFinished} // eslint-disable-line react/jsx-no-bind
|
||||
>
|
||||
<ElementClass {...props} onFinished={onFinished} />
|
||||
</LegacyContextMenu>;
|
||||
</ContextMenu>;
|
||||
|
||||
ReactDOM.render(menu, getOrCreateContainer());
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
|
|||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from '../views/rooms/EventTile';
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -15,21 +15,24 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { EventSubscription } from "fbemitter";
|
||||
import React from 'react';
|
||||
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
|
||||
|
||||
import GroupActions from '../../actions/GroupActions';
|
||||
import * as sdk from '../../index';
|
||||
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import UserTagTile from "../views/elements/UserTagTile";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import DNDTagTile from "../views/elements/DNDTagTile";
|
||||
import ActionButton from "../views/elements/ActionButton";
|
||||
|
||||
interface IGroupFilterPanelProps {
|
||||
|
||||
|
@ -127,9 +130,6 @@ class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFil
|
|||
}
|
||||
|
||||
public render() {
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
key={tag}
|
||||
|
|
|
@ -174,7 +174,7 @@ class FeaturedRoom extends React.Component {
|
|||
e.stopPropagation();
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_alias: this.props.summaryInfo.profile.canonical_alias,
|
||||
room_id: this.props.summaryInfo.room_id,
|
||||
});
|
||||
|
|
|
@ -114,7 +114,7 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
|
|||
introSection = <React.Fragment>
|
||||
<img src={logoUrl} alt={config.brand} />
|
||||
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand }) }</h1>
|
||||
<h4>{ _t("Liberate your communication") }</h4>
|
||||
<h4>{ _t("Own your conversations.") }</h4>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
|
||||
import React, { ComponentProps, createRef } from "react";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends Omit<ComponentProps<typeof AutoHideScrollbar>, "onWheel"> {
|
||||
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
||||
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
||||
// by the parent element.
|
||||
|
@ -56,6 +55,7 @@ export default class IndicatorScrollbar extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
private collectScroller = (scroller: HTMLDivElement): void => {
|
||||
this.props.wrappedRef?.(scroller);
|
||||
if (scroller && !this.scrollElement) {
|
||||
this.scrollElement = scroller;
|
||||
// Using the passive option to not block the main thread
|
||||
|
@ -186,10 +186,10 @@ export default class IndicatorScrollbar extends React.Component<IProps, IState>
|
|||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||
|
||||
return (<AutoHideScrollbar
|
||||
{...otherProps}
|
||||
ref={this.autoHideScrollbar}
|
||||
wrappedRef={this.collectScroller}
|
||||
onWheel={this.onMouseWheel}
|
||||
{...otherProps}
|
||||
>
|
||||
{ leftOverflowIndicator }
|
||||
{ children }
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
|
@ -25,31 +24,41 @@ import RoomList from "../views/rooms/RoomList";
|
|||
import CallHandler from "../../CallHandler";
|
||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import UserMenu from "./UserMenu";
|
||||
import RoomSearch from "./RoomSearch";
|
||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
|
||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||
import LeftPanelWidget from "./LeftPanelWidget";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
|
||||
import RoomListHeader from "../views/rooms/RoomListHeader";
|
||||
import { Key } from "../../Keyboard";
|
||||
import RecentlyViewedButton from "../views/rooms/RecentlyViewedButton";
|
||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import IndicatorScrollbar from "./IndicatorScrollbar";
|
||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import UserMenu from "./UserMenu";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
enum BreadcrumbsMode {
|
||||
Disabled,
|
||||
Legacy,
|
||||
Labs,
|
||||
}
|
||||
|
||||
interface IState {
|
||||
showBreadcrumbs: boolean;
|
||||
activeSpace?: Room;
|
||||
showBreadcrumbs: BreadcrumbsMode;
|
||||
activeSpace: SpaceKey;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.LeftPanel")
|
||||
|
@ -65,8 +74,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
||||
activeSpace: SpaceStore.instance.activeSpace,
|
||||
showBreadcrumbs: LeftPanel.breadcrumbsMode,
|
||||
};
|
||||
|
||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
|
@ -74,6 +83,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||
}
|
||||
|
||||
private static get breadcrumbsMode(): BreadcrumbsMode {
|
||||
if (!SettingsStore.getValue("breadcrumbs")) return BreadcrumbsMode.Disabled;
|
||||
return SettingsStore.getValue("feature_breadcrumbs_v2") ? BreadcrumbsMode.Labs : BreadcrumbsMode.Legacy;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current);
|
||||
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||
|
@ -98,7 +112,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private updateActiveSpace = (activeSpace: Room) => {
|
||||
private updateActiveSpace = (activeSpace: SpaceKey) => {
|
||||
this.setState({ activeSpace });
|
||||
};
|
||||
|
||||
|
@ -116,7 +130,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onBreadcrumbsUpdate = () => {
|
||||
const newVal = BreadcrumbsStore.instance.visible;
|
||||
const newVal = LeftPanel.breadcrumbsMode;
|
||||
if (newVal !== this.state.showBreadcrumbs) {
|
||||
this.setState({ showBreadcrumbs: newVal });
|
||||
|
||||
|
@ -300,6 +314,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRoomListKeydown = (ev: React.KeyboardEvent) => {
|
||||
if (ev.altKey || ev.ctrlKey || ev.metaKey) return;
|
||||
// we cannot handle Space as that is an activation key for all focusable elements in this widget
|
||||
if (ev.key.length === 1) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.roomSearchRef.current?.appendChar(ev.key);
|
||||
} else if (ev.key === Key.BACKSPACE) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.roomSearchRef.current?.backspace();
|
||||
}
|
||||
};
|
||||
|
||||
private selectRoom = () => {
|
||||
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile");
|
||||
if (firstRoom) {
|
||||
|
@ -308,16 +336,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private renderHeader(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_LeftPanel_userHeader">
|
||||
<UserMenu isMinimized={this.props.isMinimized} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBreadcrumbs(): React.ReactNode {
|
||||
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
|
||||
if (this.state.showBreadcrumbs === BreadcrumbsMode.Legacy && !this.props.isMinimized) {
|
||||
return (
|
||||
<IndicatorScrollbar
|
||||
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
|
||||
|
@ -334,7 +354,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
|
||||
// If we have dialer support, show a button to bring up the dial pad
|
||||
// to start a new call
|
||||
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||
if (CallHandler.instance.getSupportsPstnProtocol()) {
|
||||
dialPadButton =
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_dialPadButton", {})}
|
||||
|
@ -343,6 +363,17 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
/>;
|
||||
}
|
||||
|
||||
let rightButton: JSX.Element;
|
||||
if (this.state.showBreadcrumbs === BreadcrumbsMode.Labs) {
|
||||
rightButton = <RecentlyViewedButton />;
|
||||
} else if (this.state.activeSpace === MetaSpace.Home) {
|
||||
rightButton = <AccessibleTooltipButton
|
||||
className="mx_LeftPanel_exploreButton"
|
||||
onClick={this.onExplore}
|
||||
title={_t("Explore rooms")}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mx_LeftPanel_filterContainer"
|
||||
|
@ -350,6 +381,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ !SpaceStore.spacesEnabled && <UserMenu isPanelCollapsed={true} /> }
|
||||
<RoomSearch
|
||||
isMinimized={this.props.isMinimized}
|
||||
ref={this.roomSearchRef}
|
||||
|
@ -357,16 +389,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
/>
|
||||
|
||||
{ dialPadButton }
|
||||
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_exploreButton", {
|
||||
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
|
||||
})}
|
||||
onClick={this.onExplore}
|
||||
title={this.state.activeSpace
|
||||
? _t("Explore %(spaceName)s", { spaceName: this.state.activeSpace.name })
|
||||
: _t("Explore rooms")}
|
||||
/>
|
||||
{ rightButton }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -397,10 +420,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<div className={containerClasses} ref={this.ref}>
|
||||
<aside className="mx_LeftPanel_roomListContainer">
|
||||
{ this.renderHeader() }
|
||||
{ this.renderSearchDialExplore() }
|
||||
{ this.renderBreadcrumbs() }
|
||||
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||
{ !this.props.isMinimized && (
|
||||
<RoomListHeader
|
||||
onVisibilityChange={this.refreshStickyHeaders}
|
||||
spacePanelDisabled={!SpaceStore.spacesEnabled}
|
||||
/>
|
||||
) }
|
||||
<div className="mx_LeftPanel_roomListWrapper">
|
||||
<div
|
||||
className={roomListClasses}
|
||||
|
@ -408,6 +435,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order.
|
||||
tabIndex={-1}
|
||||
onKeyDown={this.onRoomListKeydown}
|
||||
>
|
||||
{ roomList }
|
||||
</div>
|
||||
|
|
|
@ -131,7 +131,7 @@ const LeftPanelWidget: React.FC = () => {
|
|||
}}
|
||||
className="mx_LeftPanelWidget_maximizeButton"
|
||||
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
|
||||
title={_t("Maximize")}
|
||||
title={_t("Maximise")}
|
||||
/>*/ }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,11 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { ClipboardEvent } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Key } from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
@ -27,18 +25,16 @@ import { fixupColorFonts } from '../../utils/FontManager';
|
|||
import dis from '../../dispatcher/dispatcher';
|
||||
import { IMatrixClientCreds } from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||
import { Resizer, CollapseDistributor } from '../../resizer';
|
||||
import { CollapseDistributor, Resizer } from '../../resizer';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
|
||||
import HomePage from "./HomePage";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { DefaultTagID } from "../../stores/room-list/models";
|
||||
import {
|
||||
showToast as showServerLimitToast,
|
||||
hideToast as hideServerLimitToast,
|
||||
} from "../../toasts/ServerLimitToast";
|
||||
import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import LeftPanel from "./LeftPanel";
|
||||
import CallContainer from '../views/voip/CallContainer';
|
||||
|
@ -54,7 +50,8 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
|
|||
import { IOpts } from "../../createRoom";
|
||||
import SpacePanel from "../views/spaces/SpacePanel";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import CallHandler from '../../CallHandler';
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
||||
import { OwnProfileStore } from '../../stores/OwnProfileStore';
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
|
@ -64,7 +61,8 @@ import MyGroups from "./MyGroups";
|
|||
import UserView from "./UserView";
|
||||
import GroupView from "./GroupView";
|
||||
import BackdropPanel from "./BackdropPanel";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import classNames from 'classnames';
|
||||
import GroupFilterPanel from './GroupFilterPanel';
|
||||
import CustomRoomTagPanel from './CustomRoomTagPanel';
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
|
@ -75,11 +73,8 @@ import LegacyCommunityPreview from "./LegacyCommunityPreview";
|
|||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
const MAX_PINNED_NOTICES_PER_ROOM = 2;
|
||||
|
||||
function canElementReceiveInput(el) {
|
||||
return el.tagName === "INPUT" ||
|
||||
el.tagName === "TEXTAREA" ||
|
||||
el.tagName === "SELECT" ||
|
||||
!!el.getAttribute("contenteditable");
|
||||
export function getInputableElement(el: HTMLElement): HTMLElement | null {
|
||||
return el.closest("input, textarea, select, [contenteditable=true]");
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
|
@ -140,7 +135,7 @@ interface IState {
|
|||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
* determined by the page_type property.
|
||||
*
|
||||
* Currently it's very tightly coupled with MatrixChat. We should try to do
|
||||
* Currently, it's very tightly coupled with MatrixChat. We should try to do
|
||||
* something about that.
|
||||
*
|
||||
* Components mounted below us can access the matrix client via the react context.
|
||||
|
@ -149,7 +144,6 @@ interface IState {
|
|||
class LoggedInView extends React.Component<IProps, IState> {
|
||||
static displayName = 'LoggedInView';
|
||||
|
||||
private dispatcherRef: string;
|
||||
protected readonly _matrixClient: MatrixClient;
|
||||
protected readonly _roomView: React.RefObject<any>;
|
||||
protected readonly _resizeContainer: React.RefObject<HTMLDivElement>;
|
||||
|
@ -166,7 +160,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
usageLimitDismissed: false,
|
||||
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
|
||||
activeCalls: CallHandler.instance.getAllActiveCalls(),
|
||||
};
|
||||
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
|
@ -183,7 +177,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.onCallState);
|
||||
|
||||
this.updateServerNoticeEvents();
|
||||
|
||||
|
@ -214,7 +208,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onNativeKeyDown, false);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.onCallState);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
|
@ -224,6 +218,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.resizer.detach();
|
||||
}
|
||||
|
||||
private onCallState = (): void => {
|
||||
const activeCalls = CallHandler.instance.getAllActiveCalls();
|
||||
if (activeCalls === this.state.activeCalls) return;
|
||||
this.setState({ activeCalls });
|
||||
};
|
||||
|
||||
private refreshBackgroundImage = async (): Promise<void> => {
|
||||
let backgroundImage = SettingsStore.getValue("RoomList.backgroundImage");
|
||||
if (backgroundImage) {
|
||||
|
@ -235,18 +235,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.setState({ backgroundImage });
|
||||
};
|
||||
|
||||
private onAction = (payload): void => {
|
||||
switch (payload.action) {
|
||||
case 'call_state': {
|
||||
const activeCalls = CallHandler.sharedInstance().getAllActiveCalls();
|
||||
if (activeCalls !== this.state.activeCalls) {
|
||||
this.setState({ activeCalls });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public canResetTimelineInRoom = (roomId: string) => {
|
||||
if (!this._roomView.current) {
|
||||
return true;
|
||||
|
@ -414,15 +402,13 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onPaste = (ev) => {
|
||||
let canReceiveInput = false;
|
||||
let element = ev.target;
|
||||
// test for all parents because the target can be a child of a contenteditable element
|
||||
while (!canReceiveInput && element) {
|
||||
canReceiveInput = canElementReceiveInput(element);
|
||||
element = element.parentElement;
|
||||
}
|
||||
if (!canReceiveInput) {
|
||||
private onPaste = (ev: ClipboardEvent) => {
|
||||
const element = ev.target as HTMLElement;
|
||||
const inputableElement = getInputableElement(element);
|
||||
|
||||
if (inputableElement) {
|
||||
inputableElement.focus();
|
||||
} else {
|
||||
// refocusing during a paste event will make the
|
||||
// paste end up in the newly focused element,
|
||||
// so dispatch synchronously before paste happens
|
||||
|
@ -516,6 +502,10 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
Modal.closeCurrentModal("homeKeyboardShortcut");
|
||||
handled = true;
|
||||
break;
|
||||
case NavigationAction.ToggleSpacePanel:
|
||||
dis.fire(Action.ToggleSpacePanel);
|
||||
handled = true;
|
||||
break;
|
||||
case NavigationAction.ToggleRoomSidePanel:
|
||||
if (this.props.page_type === "room_view" || this.props.page_type === "group_view") {
|
||||
dis.dispatch<ToggleRightPanelPayload>({
|
||||
|
@ -579,7 +569,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
// If the user is entering a printable character outside of an input field
|
||||
// redirect it to the composer for them.
|
||||
if (!isClickShortcut && isPrintable && !canElementReceiveInput(ev.target)) {
|
||||
if (!isClickShortcut && isPrintable && !getInputableElement(ev.target as HTMLElement)) {
|
||||
// synchronous dispatch so we focus before key generates input
|
||||
dis.fire(Action.FocusSendMessageComposer, true);
|
||||
ev.stopPropagation();
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class MainSplit extends React.Component<IProps> {
|
|||
onResize={this.onResize}
|
||||
onResizeStop={this.onResizeStop}
|
||||
className="mx_RightPanel_ResizeWrapper"
|
||||
handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
|
||||
handleClasses={{ left: "mx_ResizeHandle_horizontal" }}
|
||||
>
|
||||
{ panelView }
|
||||
</Resizable>;
|
||||
|
|
|
@ -17,10 +17,10 @@ limitations under the License.
|
|||
import React, { ComponentType, createRef } from 'react';
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { sleep, defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Error as ErrorEvent } from "matrix-analytics-events/types/typescript/Error";
|
||||
import { Screen as ScreenEvent } from "matrix-analytics-events/types/typescript/Screen";
|
||||
import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
|
||||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
import 'focus-visible';
|
||||
|
@ -35,14 +35,15 @@ import PlatformPeg from "../../PlatformPeg";
|
|||
import SdkConfig from "../../SdkConfig";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Notifier from '../../Notifier';
|
||||
|
||||
import Modal from "../../Modal";
|
||||
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import linkifyMatrix from "../../linkify-matrix";
|
||||
import * as Lifecycle from '../../Lifecycle';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
import '../../stores/LifecycleStore';
|
||||
import PageType from '../../PageTypes';
|
||||
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
@ -58,11 +59,11 @@ import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
|||
import ToastStore from "../../stores/ToastStore";
|
||||
import * as StorageManager from "../../utils/StorageManager";
|
||||
import type LoggedInViewType from "./LoggedInView";
|
||||
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import {
|
||||
showToast as showAnalyticsToast,
|
||||
hideToast as hideAnalyticsToast,
|
||||
showAnonymousAnalyticsOptInToast,
|
||||
showPseudonymousAnalyticsOptInToast,
|
||||
} from "../../toasts/AnalyticsToast";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
|
@ -77,11 +78,10 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
|||
import DialPadModal from "../views/voip/DialPadModal";
|
||||
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
|
||||
import { shouldUseLoginForWelcome } from "../../utils/pages";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import { RoomUpdateCause } from "../../stores/room-list/models";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import SecurityCustomisations from "../../customisations/Security";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||
|
@ -100,6 +100,7 @@ import Registration from './auth/Registration';
|
|||
import Login from "./auth/Login";
|
||||
import ErrorBoundary from '../views/elements/ErrorBoundary';
|
||||
import VerificationRequestToast from '../views/toasts/VerificationRequestToast';
|
||||
|
||||
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||
import SoftLogout from './auth/SoftLogout';
|
||||
|
@ -107,7 +108,15 @@ import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
|||
import { copyPlaintext } from "../../utils/strings";
|
||||
import { PosthogAnalytics } from '../../PosthogAnalytics';
|
||||
import { initSentry } from "../../sentry";
|
||||
import CallHandler from "../../CallHandler";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { showSpaceInvite } from "../../utils/space";
|
||||
import GenericToast from "../views/toasts/GenericToast";
|
||||
import InfoDialog from "../views/dialogs/InfoDialog";
|
||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -339,18 +348,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// we don't do it as react state as i'm scared about triggering needless react refreshes.
|
||||
this.subTitleStatus = '';
|
||||
|
||||
// this can technically be done anywhere but doing this here keeps all
|
||||
// the routing url path logic together.
|
||||
if (this.onAliasClick) {
|
||||
linkifyMatrix.onAliasClick = this.onAliasClick;
|
||||
}
|
||||
if (this.onUserClick) {
|
||||
linkifyMatrix.onUserClick = this.onUserClick;
|
||||
}
|
||||
if (this.onGroupClick) {
|
||||
linkifyMatrix.onGroupClick = this.onGroupClick;
|
||||
}
|
||||
|
||||
// the first thing to do is to try the token params in the query-string
|
||||
// if the session isn't soft logged out (ie: is a clean session being logged in)
|
||||
if (!Lifecycle.isSoftLogout()) {
|
||||
|
@ -389,13 +386,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("analyticsOptIn")) {
|
||||
if (SettingsStore.getValue("pseudonymousAnalyticsOptIn")) {
|
||||
Analytics.enable();
|
||||
}
|
||||
|
||||
PosthogAnalytics.instance.updateAnonymityFromSettings();
|
||||
PosthogAnalytics.instance.updatePlatformSuperProperties();
|
||||
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||
|
||||
initSentry(SdkConfig.get()["sentry"]);
|
||||
|
@ -454,7 +448,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const durationMs = this.stopPageChangeTimer();
|
||||
Analytics.trackPageChange(durationMs);
|
||||
CountlyAnalytics.instance.trackPageChange(durationMs);
|
||||
PosthogAnalytics.instance.trackPageView(durationMs);
|
||||
this.trackScreenChange(durationMs);
|
||||
}
|
||||
if (this.focusComposer) {
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
|
@ -473,6 +467,36 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||
}
|
||||
|
||||
public trackScreenChange(durationMs: number): void {
|
||||
const notLoggedInMap = {};
|
||||
notLoggedInMap[Views.LOADING] = "WebLoading";
|
||||
notLoggedInMap[Views.WELCOME] = "WebWelcome";
|
||||
notLoggedInMap[Views.LOGIN] = "WebLogin";
|
||||
notLoggedInMap[Views.REGISTER] = "WebRegister";
|
||||
notLoggedInMap[Views.FORGOT_PASSWORD] = "WebForgotPassword";
|
||||
notLoggedInMap[Views.COMPLETE_SECURITY] = "WebCompleteSecurity";
|
||||
notLoggedInMap[Views.E2E_SETUP] = "WebE2ESetup";
|
||||
notLoggedInMap[Views.SOFT_LOGOUT] = "WebSoftLogout";
|
||||
|
||||
const loggedInPageTypeMap = {};
|
||||
loggedInPageTypeMap[PageType.HomePage] = "Home";
|
||||
loggedInPageTypeMap[PageType.RoomView] = "Room";
|
||||
loggedInPageTypeMap[PageType.RoomDirectory] = "RoomDirectory";
|
||||
loggedInPageTypeMap[PageType.UserView] = "User";
|
||||
loggedInPageTypeMap[PageType.GroupView] = "Group";
|
||||
loggedInPageTypeMap[PageType.MyGroups] = "MyGroups";
|
||||
|
||||
const screenName = this.state.view === Views.LOGGED_IN ?
|
||||
loggedInPageTypeMap[this.state.page_type] :
|
||||
notLoggedInMap[this.state.view];
|
||||
|
||||
return PosthogAnalytics.instance.trackEvent<ScreenEvent>({
|
||||
eventName: "Screen",
|
||||
screenName,
|
||||
durationMs,
|
||||
});
|
||||
}
|
||||
|
||||
getFallbackHsUrl() {
|
||||
if (this.props.serverConfig && this.props.serverConfig.isDefault) {
|
||||
return this.props.config.fallback_hs_url;
|
||||
|
@ -507,8 +531,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
} else {
|
||||
dis.dispatch({ action: "view_welcome_page" });
|
||||
}
|
||||
} else if (SettingsStore.getValue("analyticsOptIn")) {
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ false);
|
||||
}
|
||||
});
|
||||
// Note we don't catch errors from this: we catch everything within
|
||||
|
@ -553,13 +575,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.setState(newState);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
// console.log(`MatrixClientPeg.onAction: ${payload.action}`);
|
||||
|
||||
// Start the onboarding process for certain actions
|
||||
if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest() &&
|
||||
ONBOARDING_FLOW_STARTERS.includes(payload.action)
|
||||
) {
|
||||
if (MatrixClientPeg.get()?.isGuest() && ONBOARDING_FLOW_STARTERS.includes(payload.action)) {
|
||||
// This will cause `payload` to be dispatched later, once a
|
||||
// sync has reached the "prepared" state. Setting a matrix ID
|
||||
// will cause a full login and sync and finally the deferred
|
||||
|
@ -597,11 +617,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
break;
|
||||
case 'logout':
|
||||
dis.dispatch({ action: "hangup_all" });
|
||||
CallHandler.instance.hangupAllCalls();
|
||||
Lifecycle.logout();
|
||||
break;
|
||||
case 'require_registration':
|
||||
startAnyRegistrationFlow(payload);
|
||||
startAnyRegistrationFlow(payload as any);
|
||||
break;
|
||||
case 'start_registration':
|
||||
if (Lifecycle.isSoftLogout()) {
|
||||
|
@ -672,12 +692,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'view_user_info':
|
||||
this.viewUser(payload.userId, payload.subAction);
|
||||
break;
|
||||
case 'view_room': {
|
||||
case Action.ViewRoom: {
|
||||
// Takes either a room ID or room alias: if switching to a room the client is already
|
||||
// known to be in (eg. user clicks on a room in the recents panel), supply the ID
|
||||
// If the user is clicking on a room in the context of the alias being presented
|
||||
// to them, supply the room alias. If both are supplied, the room ID will be ignored.
|
||||
const promise = this.viewRoom(payload);
|
||||
const promise = this.viewRoom(payload as any);
|
||||
if (payload.deferred_action) {
|
||||
promise.then(() => {
|
||||
dis.dispatch(payload.deferred_action);
|
||||
|
@ -708,16 +728,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case Action.ViewRoomDirectory: {
|
||||
if (SpaceStore.instance.activeSpace) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: SpaceStore.instance.activeSpace.roomId,
|
||||
});
|
||||
} else {
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
initialText: payload.initialText,
|
||||
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
}
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
initialText: payload.initialText,
|
||||
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this.viewSomethingBehindModal();
|
||||
|
@ -830,10 +843,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
hideToSRUsers: false,
|
||||
});
|
||||
break;
|
||||
case 'accept_cookies':
|
||||
case Action.AnonymousAnalyticsAccept:
|
||||
hideAnalyticsToast();
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
|
||||
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
hideAnalyticsToast();
|
||||
if (Analytics.canEnable()) {
|
||||
Analytics.enable();
|
||||
}
|
||||
|
@ -841,10 +854,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
CountlyAnalytics.instance.enable(/* anonymous = */ false);
|
||||
}
|
||||
break;
|
||||
case 'reject_cookies':
|
||||
case Action.AnonymousAnalyticsReject:
|
||||
hideAnalyticsToast();
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
|
||||
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
break;
|
||||
case Action.PseudonymousAnalyticsAccept:
|
||||
hideAnalyticsToast();
|
||||
SettingsStore.setValue("pseudonymousAnalyticsOptIn", null, SettingLevel.ACCOUNT, true);
|
||||
break;
|
||||
case Action.PseudonymousAnalyticsReject:
|
||||
hideAnalyticsToast();
|
||||
SettingsStore.setValue("pseudonymousAnalyticsOptIn", null, SettingLevel.ACCOUNT, false);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -907,73 +928,73 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// @param {Object=} roomInfo.oob_data Object of additional data about the room
|
||||
// that has been passed out-of-band (eg.
|
||||
// room name and avatar from an invite email)
|
||||
private viewRoom(roomInfo: IRoomInfo) {
|
||||
private async viewRoom(roomInfo: IRoomInfo) {
|
||||
this.focusComposer = true;
|
||||
|
||||
if (roomInfo.room_alias) {
|
||||
logger.log(
|
||||
`Switching to room alias ${roomInfo.room_alias} at event ` +
|
||||
roomInfo.event_id,
|
||||
);
|
||||
logger.log(`Switching to room alias ${roomInfo.room_alias} at event ${roomInfo.event_id}`);
|
||||
} else {
|
||||
logger.log(`Switching to room id ${roomInfo.room_id} at event ` +
|
||||
roomInfo.event_id,
|
||||
);
|
||||
logger.log(`Switching to room id ${roomInfo.room_id} at event ${roomInfo.event_id}`);
|
||||
}
|
||||
|
||||
// Wait for the first sync to complete so that if a room does have an alias,
|
||||
// it would have been retrieved.
|
||||
let waitFor = Promise.resolve(null);
|
||||
if (!this.firstSyncComplete) {
|
||||
if (!this.firstSyncPromise) {
|
||||
logger.warn('Cannot view a room before first sync. room_id:', roomInfo.room_id);
|
||||
return;
|
||||
}
|
||||
waitFor = this.firstSyncPromise.promise;
|
||||
await this.firstSyncPromise.promise;
|
||||
}
|
||||
|
||||
return waitFor.then(() => {
|
||||
let presentedId = roomInfo.room_alias || roomInfo.room_id;
|
||||
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
|
||||
if (room) {
|
||||
// Not all timeline events are decrypted ahead of time anymore
|
||||
// Only the critical ones for a typical UI are
|
||||
// This will start the decryption process for all events when a
|
||||
// user views a room
|
||||
room.decryptAllEvents();
|
||||
const theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||
if (theAlias) {
|
||||
presentedId = theAlias;
|
||||
// Store display alias of the presented room in cache to speed future
|
||||
// navigation.
|
||||
storeRoomAliasInCache(theAlias, room.roomId);
|
||||
}
|
||||
|
||||
// Store this as the ID of the last room accessed. This is so that we can
|
||||
// persist which room is being stored across refreshes and browser quits.
|
||||
if (localStorage) {
|
||||
localStorage.setItem('mx_last_room_id', room.roomId);
|
||||
}
|
||||
let presentedId = roomInfo.room_alias || roomInfo.room_id;
|
||||
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
|
||||
if (room) {
|
||||
// Not all timeline events are decrypted ahead of time anymore
|
||||
// Only the critical ones for a typical UI are
|
||||
// This will start the decryption process for all events when a
|
||||
// user views a room
|
||||
room.decryptAllEvents();
|
||||
const theAlias = Rooms.getDisplayAliasForRoom(room);
|
||||
if (theAlias) {
|
||||
presentedId = theAlias;
|
||||
// Store display alias of the presented room in cache to speed future
|
||||
// navigation.
|
||||
storeRoomAliasInCache(theAlias, room.roomId);
|
||||
}
|
||||
|
||||
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
|
||||
const replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
|
||||
|
||||
if (roomInfo.event_id && roomInfo.highlighted) {
|
||||
presentedId += "/" + roomInfo.event_id;
|
||||
// Store this as the ID of the last room accessed. This is so that we can
|
||||
// persist which room is being stored across refreshes and browser quits.
|
||||
if (localStorage) {
|
||||
localStorage.setItem('mx_last_room_id', room.roomId);
|
||||
}
|
||||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageType.RoomView,
|
||||
threepidInvite: roomInfo.threepid_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
forceTimeline: roomInfo.forceTimeline,
|
||||
ready: true,
|
||||
roomJustCreatedOpts: roomInfo.justCreatedOpts,
|
||||
}, () => {
|
||||
this.notifyNewScreen('room/' + presentedId, replaceLast);
|
||||
});
|
||||
}
|
||||
|
||||
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
|
||||
const replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
|
||||
|
||||
if (roomInfo.room_id === this.state.currentRoomId) {
|
||||
// if we are re-viewing the same room then copy any state we already know
|
||||
roomInfo.threepid_invite = roomInfo.threepid_invite ?? this.state.threepidInvite;
|
||||
roomInfo.oob_data = roomInfo.oob_data ?? this.state.roomOobData;
|
||||
roomInfo.forceTimeline = roomInfo.forceTimeline ?? this.state.forceTimeline;
|
||||
roomInfo.justCreatedOpts = roomInfo.justCreatedOpts ?? this.state.roomJustCreatedOpts;
|
||||
}
|
||||
|
||||
if (roomInfo.event_id && roomInfo.highlighted) {
|
||||
presentedId += "/" + roomInfo.event_id;
|
||||
}
|
||||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageType.RoomView,
|
||||
threepidInvite: roomInfo.threepid_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
forceTimeline: roomInfo.forceTimeline,
|
||||
ready: true,
|
||||
roomJustCreatedOpts: roomInfo.justCreatedOpts,
|
||||
}, () => {
|
||||
this.notifyNewScreen('room/' + presentedId, replaceLast);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1120,7 +1141,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
if (dmRooms.length > 0) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_id: dmRooms[0],
|
||||
});
|
||||
} else {
|
||||
|
@ -1191,12 +1212,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
button: _t("Leave"),
|
||||
onFinished: (shouldLeave) => {
|
||||
if (shouldLeave) {
|
||||
const d = leaveRoomBehaviour(roomId);
|
||||
leaveRoomBehaviour(roomId);
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||
|
||||
d.finally(() => modal.close());
|
||||
dis.dispatch({
|
||||
action: "after_leave_room",
|
||||
room_id: roomId,
|
||||
|
@ -1337,13 +1354,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
StorageManager.tryPersistStorage();
|
||||
|
||||
// defer the following actions by 30 seconds to not throw them at the user immediately
|
||||
await sleep(30);
|
||||
if (SettingsStore.getValue("showCookieBar") &&
|
||||
(Analytics.canEnable() || CountlyAnalytics.instance.canEnable())
|
||||
) {
|
||||
showAnalyticsToast(this.props.config.piwik?.policyUrl);
|
||||
if (PosthogAnalytics.instance.isEnabled()) {
|
||||
this.initPosthogAnalyticsToast();
|
||||
} else if (Analytics.canEnable() || CountlyAnalytics.instance.canEnable()) {
|
||||
if (SettingsStore.getValue("showCookieBar") &&
|
||||
(Analytics.canEnable() || CountlyAnalytics.instance.canEnable())
|
||||
) {
|
||||
showAnonymousAnalyticsOptInToast();
|
||||
}
|
||||
}
|
||||
|
||||
if (SdkConfig.get().mobileGuideToast) {
|
||||
// The toast contains further logic to detect mobile platforms,
|
||||
// check if it has been dismissed before, etc.
|
||||
|
@ -1351,6 +1371,34 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private showPosthogToast(analyticsOptIn: boolean) {
|
||||
showPseudonymousAnalyticsOptInToast(analyticsOptIn);
|
||||
}
|
||||
|
||||
private initPosthogAnalyticsToast() {
|
||||
// Show the analytics toast if necessary
|
||||
if (SettingsStore.getValue("pseudonymousAnalyticsOptIn") === null) {
|
||||
this.showPosthogToast(SettingsStore.getValue("analyticsOptIn", null, true));
|
||||
}
|
||||
|
||||
// Listen to changes in settings and show the toast if appropriate - this is necessary because account
|
||||
// settings can still be changing at this point in app init (due to the initial sync being cached, then
|
||||
// subsequent syncs being received from the server)
|
||||
SettingsStore.watchSetting("pseudonymousAnalyticsOptIn", null,
|
||||
(originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => {
|
||||
if (newValue === null) {
|
||||
this.showPosthogToast(SettingsStore.getValue("analyticsOptIn", null, true));
|
||||
} else {
|
||||
// It's possible for the value to change if a cached sync loads at page load, but then network
|
||||
// sync contains a new value of the flag with it set to false (e.g. another device set it since last
|
||||
// loading the page); so hide the toast.
|
||||
// (this flipping usually happens before first render so the user won't notice it; anyway flicker
|
||||
// on/off is probably better than showing the toast again when the user already dismissed it)
|
||||
hideAnalyticsToast();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showScreenAfterLogin() {
|
||||
// If screenAfterLogin is set, use that, then null it so that a second login will
|
||||
// result in view_home_page, _user_settings or _room_directory
|
||||
|
@ -1374,7 +1422,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private viewLastRoom() {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_id: localStorage.getItem('mx_last_room_id'),
|
||||
});
|
||||
}
|
||||
|
@ -1472,6 +1520,61 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
showNotificationsToast(false);
|
||||
}
|
||||
|
||||
if (!localStorage.getItem("mx_seen_ia_1.1_changes_toast")) {
|
||||
const key = "IA_1.1_TOAST";
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key,
|
||||
title: _t("Testing small changes"),
|
||||
props: {
|
||||
description: _t("Your feedback is wanted as we try out some design changes."),
|
||||
acceptLabel: _t("More info"),
|
||||
onAccept: () => {
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("We're testing some design changes"),
|
||||
description: <>
|
||||
<img
|
||||
src={require("../../../res/img/ia-design-changes.png")}
|
||||
width="636"
|
||||
height="303"
|
||||
alt=""
|
||||
/>
|
||||
<p>{ _t(
|
||||
"Your ongoing feedback would be very welcome, so if you see anything " +
|
||||
"different you want to comment on, <a>please let us know about it</a>. " +
|
||||
"Click your avatar to find a quick feedback link.",
|
||||
{},
|
||||
{
|
||||
a: sub => <AccessibleButton
|
||||
kind="link"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
|
||||
}}
|
||||
>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
) }</p>
|
||||
<p>{ _t("If you'd like to preview or test some potential upcoming changes, " +
|
||||
"there's an option in feedback to let us contact you.") }</p>
|
||||
</>,
|
||||
}, "mx_DialogDesignChanges_wrapper");
|
||||
localStorage.setItem("mx_seen_ia_1.1_changes_toast", "true");
|
||||
ToastStore.sharedInstance().dismissToast(key);
|
||||
},
|
||||
rejectLabel: _t("Dismiss"),
|
||||
onReject: () => {
|
||||
localStorage.setItem("mx_seen_ia_1.1_changes_toast", "true");
|
||||
ToastStore.sharedInstance().dismissToast(key);
|
||||
},
|
||||
},
|
||||
icon: "labs",
|
||||
component: GenericToast,
|
||||
priority: 9,
|
||||
});
|
||||
}
|
||||
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
this.setState({
|
||||
ready: true,
|
||||
|
@ -1524,17 +1627,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
|
||||
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
|
||||
PosthogAnalytics.instance.trackEvent<ErrorEvent>({
|
||||
eventName: "Error",
|
||||
domain: "E2EE",
|
||||
name: errorCode,
|
||||
});
|
||||
}, (errorCode) => {
|
||||
// Map JS-SDK error codes to tracker codes for aggregation
|
||||
switch (errorCode) {
|
||||
case 'MEGOLM_UNKNOWN_INBOUND_SESSION_ID':
|
||||
return 'olm_keys_not_sent_error';
|
||||
return 'OlmKeysNotSentError';
|
||||
case 'OLM_UNKNOWN_MESSAGE_INDEX':
|
||||
return 'olm_index_error';
|
||||
return 'OlmIndexError';
|
||||
case undefined:
|
||||
return 'unexpected_error';
|
||||
return 'OlmUnspecifiedError';
|
||||
default:
|
||||
return 'unspecified_error';
|
||||
return 'UnknownError';
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1789,7 +1897,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
const payload = {
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
event_id: eventId,
|
||||
via_servers: via,
|
||||
// If an event ID is given in the URL hash, notify RoomViewStore to mark
|
||||
|
@ -1842,28 +1950,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
this.setPageSubtitle();
|
||||
}
|
||||
|
||||
onAliasClick(event: MouseEvent, alias: string) {
|
||||
event.preventDefault();
|
||||
dis.dispatch({ action: 'view_room', room_alias: alias });
|
||||
}
|
||||
|
||||
onUserClick(event: MouseEvent, userId: string) {
|
||||
event.preventDefault();
|
||||
|
||||
const member = new RoomMember(null, userId);
|
||||
if (!member) { return; }
|
||||
dis.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
member: member,
|
||||
});
|
||||
}
|
||||
|
||||
onGroupClick(event: MouseEvent, groupId: string) {
|
||||
event.preventDefault();
|
||||
dis.dispatch({ action: 'view_group', group_id: groupId });
|
||||
}
|
||||
|
||||
onLogoutClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
|
||||
dis.dispatch({
|
||||
action: 'logout',
|
||||
|
|
|
@ -28,7 +28,7 @@ import { wantsDateSeparator } from '../../DateUtils';
|
|||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import { _t } from "../../languageHandler";
|
||||
import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile";
|
||||
import { hasText } from "../../TextForEvent";
|
||||
|
@ -179,6 +179,7 @@ interface IProps {
|
|||
getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations;
|
||||
|
||||
hideThreadedMessages?: boolean;
|
||||
disableGrouping?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -198,6 +199,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
static defaultProps = {
|
||||
disableGrouping: false,
|
||||
};
|
||||
|
||||
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||
// to manage its animations
|
||||
private readonly readReceiptMap: Record<string, object> = {};
|
||||
|
@ -652,7 +657,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
for (const Grouper of groupers) {
|
||||
if (Grouper.canStartGroup(this, mxEv)) {
|
||||
if (Grouper.canStartGroup(this, mxEv) && !this.props.disableGrouping) {
|
||||
grouper = new Grouper(
|
||||
this,
|
||||
mxEv,
|
||||
|
|
|
@ -24,7 +24,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
|
|||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from "../views/rooms/EventTile";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
|
||||
interface IProps {
|
||||
|
@ -39,7 +39,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
|||
static contextType = RoomContext;
|
||||
render() {
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{ _t('You’re all caught up') }</h2>
|
||||
<h2>{ _t("You're all caught up") }</h2>
|
||||
<p>{ _t('You have no visible notifications.') }</p>
|
||||
</div>);
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
|||
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 { throttle } from 'lodash';
|
||||
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
|
@ -50,10 +49,12 @@ import ThreadPanel from "./ThreadPanel";
|
|||
import NotificationPanel from "./NotificationPanel";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import { throttle } from 'lodash';
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads';
|
||||
import TimelineCard from '../views/right_panel/TimelineCard';
|
||||
|
||||
interface IProps {
|
||||
room?: Room; // if showing panels for a given room, this is set
|
||||
|
@ -77,6 +78,7 @@ interface IState {
|
|||
event: MatrixEvent;
|
||||
initialEvent?: MatrixEvent;
|
||||
initialEventHighlighted?: boolean;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RightPanel")
|
||||
|
@ -92,6 +94,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
phase: this.getPhaseFromProps(),
|
||||
isUserPrivilegedInGroup: null,
|
||||
member: this.getUserForPanel(),
|
||||
searchQuery: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -198,7 +201,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
const isChangingRoom = payload.action === 'view_room' && payload.room_id !== this.props.room.roomId;
|
||||
const isChangingRoom = payload.action === Action.ViewRoom && payload.room_id !== this.props.room.roomId;
|
||||
const isViewingThread = this.state.phase === RightPanelPhases.ThreadView;
|
||||
if (isChangingRoom && isViewingThread) {
|
||||
dispatchShowThreadsPanelEvent();
|
||||
|
@ -248,6 +251,10 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onSearchQueryChanged = (searchQuery: string): void => {
|
||||
this.setState({ searchQuery });
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
let panel = <div />;
|
||||
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
||||
|
@ -255,7 +262,13 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
switch (this.state.phase) {
|
||||
case RightPanelPhases.RoomMemberList:
|
||||
if (roomId) {
|
||||
panel = <MemberList roomId={roomId} key={roomId} onClose={this.onClose} />;
|
||||
panel = <MemberList
|
||||
roomId={roomId}
|
||||
key={roomId}
|
||||
onClose={this.onClose}
|
||||
searchQuery={this.state.searchQuery}
|
||||
onSearchQueryChanged={this.onSearchQueryChanged}
|
||||
/>;
|
||||
}
|
||||
break;
|
||||
case RightPanelPhases.SpaceMemberList:
|
||||
|
@ -263,6 +276,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
roomId={this.state.space ? this.state.space.roomId : roomId}
|
||||
key={this.state.space ? this.state.space.roomId : roomId}
|
||||
onClose={this.onClose}
|
||||
searchQuery={this.state.searchQuery}
|
||||
onSearchQueryChanged={this.onSearchQueryChanged}
|
||||
/>;
|
||||
break;
|
||||
|
||||
|
@ -320,7 +335,17 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case RightPanelPhases.Timeline:
|
||||
if (!SettingsStore.getValue("feature_maximised_widgets")) break;
|
||||
panel = <TimelineCard
|
||||
room={this.props.room}
|
||||
timelineSet={this.props.room.getUnfilteredTimelineSet()}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
onClose={this.onClose}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
/>;
|
||||
break;
|
||||
case RightPanelPhases.FilePanel:
|
||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||
break;
|
||||
|
@ -341,7 +366,9 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
|||
panel = <ThreadPanel
|
||||
roomId={roomId}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
onClose={this.onClose} />;
|
||||
onClose={this.onClose}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.RoomSummary:
|
||||
|
|
|
@ -19,7 +19,6 @@ import React from "react";
|
|||
import { IFieldType, IInstance, IProtocol, IPublicRoomsChunkRoom } from "matrix-js-sdk/src/client";
|
||||
import { Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||
import { IRoomDirectoryOptions } from "matrix-js-sdk/src/@types/requests";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
@ -49,6 +48,9 @@ import Spinner from "../views/elements/Spinner";
|
|||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { getDisplayAliasForAliasSet } from "../../Rooms";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 800;
|
||||
|
||||
|
@ -492,7 +494,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
private showRoom(room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
|
||||
this.onFinished();
|
||||
const payload: ActionPayload = {
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
auto_join: autoJoin,
|
||||
should_peek: shouldPeek,
|
||||
_type: "room_directory", // instrumentation
|
||||
|
@ -588,9 +590,12 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
||||
|
||||
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
|
||||
return [
|
||||
return <div
|
||||
key={room.room_id}
|
||||
role="listitem"
|
||||
className="mx_RoomDirectory_listItem"
|
||||
>
|
||||
<div
|
||||
key={`${room.room_id}_avatar`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
|
@ -602,9 +607,8 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
idName={name}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
</div>,
|
||||
</div>
|
||||
<div
|
||||
key={`${room.room_id}_description`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomDescription"
|
||||
>
|
||||
|
@ -625,30 +629,27 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
>
|
||||
{ getDisplayAliasForRoom(room) }
|
||||
</div>
|
||||
</div>,
|
||||
</div>
|
||||
<div
|
||||
key={`${room.room_id}_memberCount`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomMemberCount"
|
||||
>
|
||||
{ room.num_joined_members }
|
||||
</div>,
|
||||
</div>
|
||||
<div
|
||||
key={`${room.room_id}_preview`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
className="mx_RoomDirectory_preview"
|
||||
>
|
||||
{ previewButton }
|
||||
</div>,
|
||||
</div>
|
||||
<div
|
||||
key={`${room.room_id}_join`}
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_join"
|
||||
>
|
||||
{ joinOrViewButton }
|
||||
</div>,
|
||||
];
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
private stringLooksLikeId(s: string, fieldType: IFieldType) {
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
|
@ -28,7 +27,9 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
|
|||
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { isMac } from "../../Keyboard";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -41,12 +42,11 @@ interface IProps {
|
|||
interface IState {
|
||||
query: string;
|
||||
focused: boolean;
|
||||
inSpaces: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomSearch")
|
||||
export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private readonly dispatcherRef: string;
|
||||
private inputRef: React.RefObject<HTMLInputElement> = createRef();
|
||||
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||
|
||||
|
@ -56,13 +56,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
this.state = {
|
||||
query: "",
|
||||
focused: false,
|
||||
inSpaces: false,
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
|
||||
|
@ -83,17 +81,10 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||
SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
||||
}
|
||||
|
||||
private onSpaces = (spaces: Room[]) => {
|
||||
this.setState({
|
||||
inSpaces: spaces.length > 0,
|
||||
});
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === 'view_room' && payload.clear_search) {
|
||||
if (payload.action === Action.ViewRoom && payload.clear_search) {
|
||||
this.clearInput();
|
||||
} else if (payload.action === 'focus_room_filter' && this.inputRef.current) {
|
||||
this.inputRef.current.focus();
|
||||
|
@ -145,9 +136,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
public focus(): void {
|
||||
public focus = (): void => {
|
||||
this.inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const classes = classNames({
|
||||
|
@ -162,13 +153,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
|
||||
});
|
||||
|
||||
let placeholder = _t("Filter");
|
||||
if (this.state.inSpaces) {
|
||||
placeholder = _t("Filter all spaces");
|
||||
}
|
||||
|
||||
let icon = (
|
||||
<div className='mx_RoomSearch_icon' />
|
||||
<div className="mx_RoomSearch_icon" onClick={this.focus} />
|
||||
);
|
||||
let input = (
|
||||
<input
|
||||
|
@ -180,7 +166,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={placeholder}
|
||||
placeholder={_t("Filter")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
@ -192,6 +178,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
onClick={this.clearInput}
|
||||
/>
|
||||
);
|
||||
let shortcutPrompt = <div className="mx_RoomSearch_shortcutPrompt" onClick={this.focus}>
|
||||
{ isMac ? "⌘ K" : "Ctrl K" }
|
||||
</div>;
|
||||
|
||||
if (this.props.isMinimized) {
|
||||
icon = (
|
||||
|
@ -203,14 +192,28 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
input = null;
|
||||
clearButton = null;
|
||||
shortcutPrompt = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ icon }
|
||||
{ input }
|
||||
{ shortcutPrompt }
|
||||
{ clearButton }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public appendChar(char: string): void {
|
||||
this.setState({
|
||||
query: this.state.query + char,
|
||||
});
|
||||
}
|
||||
|
||||
public backspace(): void {
|
||||
this.setState({
|
||||
query: this.state.query.substring(0, this.state.query.length - 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,10 +27,10 @@ import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
import { ISearchResults } from 'matrix-js-sdk/src/@types/search';
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { throttle } from "lodash";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
||||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
@ -38,7 +38,7 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
|||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
import CallHandler, { PlaceCallType } from '../../CallHandler';
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, { searchPagination } from '../../Searching';
|
||||
|
@ -48,7 +48,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
|||
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
|
@ -70,6 +70,7 @@ import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
|||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
|
@ -82,6 +83,7 @@ import SpaceRoomView from "./SpaceRoomView";
|
|||
import { IOpts } from "../../createRoom";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { throttle } from "lodash";
|
||||
import ErrorDialog from '../views/dialogs/ErrorDialog';
|
||||
import SearchResultTile from '../views/rooms/SearchResultTile';
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
|
@ -90,10 +92,13 @@ import RoomStatusBar from "./RoomStatusBar";
|
|||
import MessageComposer from '../views/rooms/MessageComposer';
|
||||
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
|
||||
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads';
|
||||
import { fetchInitialEvent } from "../../utils/EventUtils";
|
||||
import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import AppsDrawer from '../views/rooms/AppsDrawer';
|
||||
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { RightPanelPhases } from '../../stores/RightPanelStorePhases';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -118,6 +123,13 @@ interface IRoomProps extends MatrixClientProps {
|
|||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
// This defines the content of the mainSplit.
|
||||
// If the mainSplit does not contain the Timeline, the chat is shown in the right panel.
|
||||
enum MainSplitContentType {
|
||||
Timeline,
|
||||
MaximisedWidget,
|
||||
// Video
|
||||
}
|
||||
export interface IRoomState {
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
|
@ -187,6 +199,7 @@ export interface IRoomState {
|
|||
rejecting?: boolean;
|
||||
rejectError?: Error;
|
||||
hasPinnedWidgets?: boolean;
|
||||
mainSplitContentType?: MainSplitContentType;
|
||||
dragCounter: number;
|
||||
// whether or not a spaces context switch brought us here,
|
||||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
|
@ -253,6 +266,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
showAvatarChanges: true,
|
||||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
mainSplitContentType: MainSplitContentType.Timeline,
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
|
@ -305,18 +319,59 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
private onWidgetStoreUpdate = () => {
|
||||
if (this.state.room) {
|
||||
this.checkWidgets(this.state.room);
|
||||
if (!this.state.room) return;
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
||||
private onWidgetEchoStoreUpdate = () => {
|
||||
if (!this.state.room) return;
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
||||
private onWidgetLayoutChange = () => {
|
||||
if (!this.state.room) return;
|
||||
if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) {
|
||||
// Show chat in right panel when a widget is maximised
|
||||
dis.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.Timeline,
|
||||
});
|
||||
}
|
||||
this.checkWidgets(this.state.room);
|
||||
this.checkRightPanel(this.state.room);
|
||||
};
|
||||
|
||||
private checkWidgets = (room) => {
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top).length > 0,
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(room),
|
||||
mainSplitContentType: this.getMainSplitContentType(room),
|
||||
showApps: this.shouldShowApps(room),
|
||||
});
|
||||
};
|
||||
|
||||
private getMainSplitContentType = (room) => {
|
||||
// TODO-video check if video should be displayed in main panel
|
||||
return (WidgetLayoutStore.instance.hasMaximisedWidget(room))
|
||||
? MainSplitContentType.MaximisedWidget
|
||||
: MainSplitContentType.Timeline;
|
||||
};
|
||||
|
||||
private checkRightPanel = (room) => {
|
||||
// This is a hack to hide the chat. This should not be necessary once the right panel
|
||||
// phase is stored per room. (need to be done after check widget so that mainSplitContentType is updated)
|
||||
if (
|
||||
RightPanelStore.getSharedInstance().roomPanelPhase === RightPanelPhases.Timeline &&
|
||||
this.state.showRightPanel &&
|
||||
!WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)
|
||||
) {
|
||||
// Two timelines are shown prevent this by hiding the right panel
|
||||
dis.dispatch({
|
||||
action: Action.ToggleRightPanel,
|
||||
type: "room",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onReadReceiptsChange = () => {
|
||||
this.setState({
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
|
||||
|
@ -392,6 +447,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
} else {
|
||||
newState.initialEventId = initialEventId;
|
||||
newState.isInitialEventHighlighted = RoomViewStore.isInitialEventHighlighted();
|
||||
|
||||
if (thread && initialEvent?.isThreadRoot) {
|
||||
dispatchShowThreadEvent(
|
||||
thread.rootEvent,
|
||||
initialEvent,
|
||||
RoomViewStore.isInitialEventHighlighted(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -503,18 +566,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onWidgetEchoStoreUpdate = () => {
|
||||
if (!this.state.room) return;
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(this.state.room, Container.Top).length > 0,
|
||||
showApps: this.shouldShowApps(this.state.room),
|
||||
});
|
||||
};
|
||||
|
||||
private onWidgetLayoutChange = () => {
|
||||
this.onWidgetEchoStoreUpdate(); // we cheat here by calling the thing that matters
|
||||
};
|
||||
|
||||
private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean) {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
|
@ -582,15 +633,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
// Check if user has previously chosen to hide the app drawer for this
|
||||
// room. If so, do not show apps
|
||||
const hideWidgetDrawer = localStorage.getItem(
|
||||
room.roomId + "_hide_widget_drawer");
|
||||
const hideWidgetKey = room.roomId + "_hide_widget_drawer";
|
||||
const hideWidgetDrawer = localStorage.getItem(hideWidgetKey);
|
||||
|
||||
// This is confusing, but it means to say that we default to the tray being
|
||||
// hidden unless the user clicked to open it.
|
||||
const isManuallyShown = hideWidgetDrawer === "false";
|
||||
// If unset show the Tray
|
||||
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
|
||||
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false": true;
|
||||
|
||||
const widgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
|
||||
return widgets.length > 0 || isManuallyShown;
|
||||
return isManuallyShown && widgets.length > 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -619,6 +670,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.onCallState);
|
||||
if (this.roomView.current) {
|
||||
const roomView = this.roomView.current;
|
||||
if (!roomView.ondrop) {
|
||||
|
@ -649,6 +701,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.onCallState);
|
||||
|
||||
// update the scroll map before we get unmounted
|
||||
if (this.state.roomId) {
|
||||
RoomScrollStateStore.setScrollState(this.state.roomId, this.getScrollState());
|
||||
|
@ -721,7 +775,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
private onUserScroll = () => {
|
||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.room.roomId,
|
||||
event_id: this.state.initialEventId,
|
||||
highlighted: false,
|
||||
|
@ -772,6 +826,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onCallState = (roomId: string): void => {
|
||||
// don't filter out payloads for room IDs other than props.room because
|
||||
// we may be interested in the conf 1:1 room
|
||||
|
||||
if (!roomId) return;
|
||||
const call = this.getCallForRoom();
|
||||
this.setState({ callState: call ? call.state : null });
|
||||
};
|
||||
|
||||
private onAction = payload => {
|
||||
switch (payload.action) {
|
||||
case 'message_sent':
|
||||
|
@ -781,11 +844,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.injectSticker(
|
||||
payload.data.content.url,
|
||||
payload.data.content.info,
|
||||
payload.data.description || payload.data.name);
|
||||
payload.data.description || payload.data.name,
|
||||
payload.data.threadId);
|
||||
break;
|
||||
case 'picture_snapshot':
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
[payload.file], this.state.room.roomId, this.context);
|
||||
[payload.file], this.state.room.roomId, null, this.context);
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
case Action.UploadStarted:
|
||||
|
@ -793,21 +857,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
case Action.UploadCanceled:
|
||||
this.forceUpdate();
|
||||
break;
|
||||
case 'call_state': {
|
||||
// don't filter out payloads for room IDs other than props.room because
|
||||
// we may be interested in the conf 1:1 room
|
||||
|
||||
if (!payload.room_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const call = this.getCallForRoom();
|
||||
|
||||
this.setState({
|
||||
callState: call ? call.state : null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'appsDrawer':
|
||||
this.setState({
|
||||
showApps: payload.show,
|
||||
|
@ -830,7 +879,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
setImmediate(() => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
deferred_action: payload,
|
||||
});
|
||||
|
@ -880,7 +929,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
case "scroll_to_bottom":
|
||||
if (payload.timelineRenderingType === this.context.timelineRenderingType) {
|
||||
if (payload.timelineRenderingType === TimelineRenderingType.Room) {
|
||||
this.messagePanel?.jumpToLiveTimeline();
|
||||
}
|
||||
break;
|
||||
|
@ -941,7 +990,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
CHAT_EFFECTS.forEach(effect => {
|
||||
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
|
||||
dis.dispatch({ action: `effects.${effect.command}` });
|
||||
// For initial threads launch, chat effects are disabled
|
||||
// see #19731
|
||||
if (!SettingsStore.getValue("feature_thread") || !ev.isThreadRelation) {
|
||||
dis.dispatch({ action: `effects.${effect.command}` });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -971,7 +1024,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (this.unmounted) return;
|
||||
// Attach a widget store listener only when we get a room
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
|
||||
this.onWidgetLayoutChange(); // provoke an update
|
||||
|
||||
this.calculatePeekRules(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
|
@ -980,6 +1032,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.updateE2EStatus(room);
|
||||
this.updatePermissions(room);
|
||||
this.checkWidgets(room);
|
||||
this.checkRightPanel(room);
|
||||
|
||||
this.setState({
|
||||
liveTimeline: room.getLiveTimeline(),
|
||||
|
@ -1112,12 +1165,21 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRoomStateEvents = (ev: MatrixEvent, state) => {
|
||||
private onRoomStateEvents = (ev: MatrixEvent, state: RoomState) => {
|
||||
// ignore if we don't have a room yet
|
||||
if (!this.state.room || this.state.room.roomId !== state.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.getType() === EventType.RoomCanonicalAlias) {
|
||||
// re-view the room so MatrixChat can manage the alias in the URL properly
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.room.roomId,
|
||||
});
|
||||
return; // this event cannot affect permissions so bail
|
||||
}
|
||||
|
||||
this.updatePermissions(this.state.room);
|
||||
};
|
||||
|
||||
|
@ -1209,7 +1271,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
dis.dispatch({
|
||||
action: 'do_after_sync_prepared',
|
||||
deferred_action: {
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.getRoomId(),
|
||||
},
|
||||
});
|
||||
|
@ -1291,7 +1353,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
ev.dataTransfer.files, this.state.room.roomId, this.context,
|
||||
ev.dataTransfer.files, this.state.room.roomId, null, this.context,
|
||||
);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
|
||||
|
@ -1301,13 +1363,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
};
|
||||
|
||||
private injectSticker(url: string, info: object, text: string) {
|
||||
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
|
||||
if (this.context.isGuest()) {
|
||||
dis.dispatch({ action: 'require_registration' });
|
||||
return;
|
||||
}
|
||||
|
||||
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, this.context)
|
||||
ContentMessages.sharedInstance()
|
||||
.sendStickerContentToRoom(url, this.state.room.roomId, threadId, info, text, this.context)
|
||||
.then(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// Let the staus bar handle this
|
||||
|
@ -1477,16 +1540,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return ret;
|
||||
}
|
||||
|
||||
private onCallPlaced = (type: PlaceCallType) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: type,
|
||||
room_id: this.state.room.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onSettingsClick = () => {
|
||||
dis.dispatch({ action: "open_room_settings" });
|
||||
private onCallPlaced = (type: CallType): void => {
|
||||
CallHandler.instance.placeCall(this.state.room?.roomId, type);
|
||||
};
|
||||
|
||||
private onAppsClick = () => {
|
||||
|
@ -1589,7 +1644,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// an event will take care of both clearing the URL fragment and
|
||||
// jumping to the bottom
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.room.roomId,
|
||||
});
|
||||
} else {
|
||||
|
@ -1698,7 +1753,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (!this.state.room) {
|
||||
return null;
|
||||
}
|
||||
return CallHandler.sharedInstance().getCallForRoom(this.state.room.roomId);
|
||||
return CallHandler.instance.getCallForRoom(this.state.room.roomId);
|
||||
}
|
||||
|
||||
// this has to be a proper method rather than an unnamed function,
|
||||
|
@ -2079,6 +2134,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
const showRightPanel = this.state.room && this.state.showRightPanel;
|
||||
|
||||
const rightPanel = showRightPanel
|
||||
? <RightPanel
|
||||
room={this.state.room}
|
||||
|
@ -2097,6 +2153,52 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
||||
|
||||
// Decide what to show in the main split
|
||||
let mainSplitBody = <React.Fragment>
|
||||
{ auxPanel }
|
||||
<div className={timelineClasses}>
|
||||
{ fileDropTarget }
|
||||
{ topUnreadMessagesBar }
|
||||
{ jumpToBottom }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
</div>
|
||||
{ statusBarArea }
|
||||
{ previewBar }
|
||||
{ messageComposer }
|
||||
</React.Fragment>;
|
||||
|
||||
switch (this.state.mainSplitContentType) {
|
||||
case MainSplitContentType.Timeline:
|
||||
// keep the timeline in as the mainSplitBody
|
||||
break;
|
||||
case MainSplitContentType.MaximisedWidget:
|
||||
if (!SettingsStore.getValue("feature_maximised_widgets")) break;
|
||||
mainSplitBody = <AppsDrawer
|
||||
room={this.state.room}
|
||||
userId={this.context.credentials.userId}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showApps={true}
|
||||
/>;
|
||||
break;
|
||||
// TODO-video MainSplitContentType.Video:
|
||||
// break;
|
||||
}
|
||||
let excludedRightPanelPhaseButtons = [RightPanelPhases.Timeline];
|
||||
let onAppsClick = this.onAppsClick;
|
||||
let onForgetClick = this.onForgetClick;
|
||||
let onSearchClick = this.onSearchClick;
|
||||
if (this.state.mainSplitContentType === MainSplitContentType.MaximisedWidget) {
|
||||
// Disable phase buttons and action button to have a simplified header when a widget is maximised
|
||||
// and enable (not disable) the RightPanelPhases.Timeline button
|
||||
excludedRightPanelPhaseButtons = [
|
||||
RightPanelPhases.ThreadPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
];
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
}
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
|
@ -2109,27 +2211,17 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onSearchClick={onSearchClick}
|
||||
onForgetClick={(myMembership === "leave") ? onForgetClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
||||
onAppsClick={this.state.hasPinnedWidgets ? onAppsClick : null}
|
||||
appsShown={this.state.showApps}
|
||||
onCallPlaced={this.onCallPlaced}
|
||||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||
/>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
<div className="mx_RoomView_body">
|
||||
{ auxPanel }
|
||||
<div className={timelineClasses}>
|
||||
{ fileDropTarget }
|
||||
{ topUnreadMessagesBar }
|
||||
{ jumpToBottom }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
</div>
|
||||
{ statusBarArea }
|
||||
{ previewBar }
|
||||
{ messageComposer }
|
||||
{ mainSplitBody }
|
||||
</div>
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -750,6 +750,8 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
const minHeight = sn.clientHeight;
|
||||
const height = Math.max(minHeight, contentHeight);
|
||||
this.pages = Math.ceil(height / PAGE_SIZE);
|
||||
const displayScrollbar = contentHeight > minHeight;
|
||||
sn.dataset.scrollbar = displayScrollbar.toString();
|
||||
this.bottomGrowth = 0;
|
||||
const newHeight = `${this.getListHeight()}px`;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import { Key } from '../../Keyboard';
|
|||
import dis from '../../dispatcher/dispatcher';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
|
||||
interface IProps {
|
||||
onSearch?: (query: string) => void;
|
||||
|
@ -78,7 +79,7 @@ export default class SearchBox extends React.Component<IProps, IState> {
|
|||
if (!this.props.enableRoomSearchFocus) return;
|
||||
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
case Action.ViewRoom:
|
||||
if (this.search.current && payload.clear_search) {
|
||||
this.clearSearch();
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ import { mediaFromMxc } from "../../customisations/Media";
|
|||
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||
import { getChildOrder } from "../../stores/SpaceStore";
|
||||
import { getChildOrder } from "../../stores/spaces/SpaceStore";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { linkifyElement } from "../../HtmlUtils";
|
||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
|
@ -68,7 +68,6 @@ interface IProps {
|
|||
initialText?: string;
|
||||
additionalButtons?: ReactNode;
|
||||
showRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, roomType?: RoomType): void;
|
||||
joinRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): void;
|
||||
}
|
||||
|
||||
interface ITileProps {
|
||||
|
@ -78,7 +77,7 @@ interface ITileProps {
|
|||
numChildRooms?: number;
|
||||
hasPermissions?: boolean;
|
||||
onViewRoomClick(): void;
|
||||
onJoinRoomClick(): void;
|
||||
onJoinRoomClick(): Promise<unknown>;
|
||||
onToggleClick?(): void;
|
||||
}
|
||||
|
||||
|
@ -115,9 +114,9 @@ const Tile: React.FC<ITileProps> = ({
|
|||
setBusy(true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onJoinRoomClick();
|
||||
setJoinedRoom(await awaitRoomDownSync(cli, room.room_id));
|
||||
setBusy(false);
|
||||
onJoinRoomClick().then(() => awaitRoomDownSync(cli, room.room_id)).then(setJoinedRoom).finally(() => {
|
||||
setBusy(false);
|
||||
});
|
||||
};
|
||||
|
||||
let button;
|
||||
|
@ -141,7 +140,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||
>
|
||||
{ _t("View") }
|
||||
</AccessibleButton>;
|
||||
} else if (onJoinClick) {
|
||||
} else {
|
||||
button = <AccessibleButton
|
||||
onClick={onJoinClick}
|
||||
kind="primary"
|
||||
|
@ -343,7 +342,7 @@ export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
|
|||
});
|
||||
};
|
||||
|
||||
export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): void => {
|
||||
export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): Promise<unknown> => {
|
||||
// Don't let the user view a room they won't be able to either peek or join:
|
||||
// fail earlier so they don't have to click back to the directory.
|
||||
if (cli.isGuest()) {
|
||||
|
@ -351,11 +350,15 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
|
|||
return;
|
||||
}
|
||||
|
||||
cli.joinRoom(roomId, {
|
||||
const prom = cli.joinRoom(roomId, {
|
||||
viaServers: Array.from(hierarchy.viaMap.get(roomId) || []),
|
||||
}).catch(err => {
|
||||
});
|
||||
|
||||
prom.catch(err => {
|
||||
RoomViewStore.showJoinRoomError(err, roomId);
|
||||
});
|
||||
|
||||
return prom;
|
||||
};
|
||||
|
||||
interface IHierarchyLevelProps {
|
||||
|
@ -365,7 +368,7 @@ interface IHierarchyLevelProps {
|
|||
parents: Set<string>;
|
||||
selectedMap?: Map<string, Set<string>>;
|
||||
onViewRoomClick(roomId: string, roomType?: RoomType): void;
|
||||
onJoinRoomClick(roomId: string): void;
|
||||
onJoinRoomClick(roomId: string): Promise<unknown>;
|
||||
onToggleClick?(parentId: string, childId: string): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPan
|
|||
import { useStateArray } from "../../hooks/useStateArray";
|
||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||
import {
|
||||
shouldShowSpaceInvite,
|
||||
shouldShowSpaceSettings,
|
||||
showAddExistingRooms,
|
||||
showCreateNewRoom,
|
||||
|
@ -56,9 +57,9 @@ import {
|
|||
showSpaceInvite,
|
||||
showSpaceSettings,
|
||||
} from "../../utils/space";
|
||||
import SpaceHierarchy, { joinRoom, showRoom } from "./SpaceHierarchy";
|
||||
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import FacePile from "../views/elements/FacePile";
|
||||
import {
|
||||
AddExistingToSpace,
|
||||
|
@ -127,6 +128,7 @@ const useMyRoomMembership = (room: Room) => {
|
|||
};
|
||||
|
||||
const SpaceInfo = ({ space }: { space: Room }) => {
|
||||
// summary will begin as undefined whilst loading and go null if it fails to load.
|
||||
const summary = useAsyncMemo(async () => {
|
||||
if (space.getMyMembership() !== "invite") return;
|
||||
try {
|
||||
|
@ -155,7 +157,7 @@ const SpaceInfo = ({ space }: { space: Room }) => {
|
|||
memberSection = <span className="mx_SpaceRoomView_info_memberCount">
|
||||
{ _t("%(count)s members", { count: summary.num_joined_members }) }
|
||||
</span>;
|
||||
} else if (summary === null) {
|
||||
} else if (summary !== undefined) { // summary is not still loading
|
||||
memberSection = <RoomMemberCount room={space}>
|
||||
{ (count) => count > 0 ? (
|
||||
<AccessibleButton
|
||||
|
@ -394,7 +396,7 @@ const SpaceLandingAddButton = ({ space }) => {
|
|||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
iconClassName="mx_RoomList_iconAddExistingRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -438,9 +440,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
|||
const userId = cli.getUserId();
|
||||
|
||||
let inviteButton;
|
||||
if (((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) &&
|
||||
shouldShowComponent(UIComponent.InviteUsers)
|
||||
) {
|
||||
if (shouldShowSpaceInvite(space) && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
|
@ -507,7 +507,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
|||
) }
|
||||
</RoomTopic>
|
||||
|
||||
<SpaceHierarchy space={space} showRoom={showRoom} joinRoom={joinRoom} additionalButtons={addRoomButton} />
|
||||
<SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -14,29 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import MatrixClientContext from '../../contexts/MatrixClientContext';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuButton';
|
||||
import ContextMenu, { useContextMenu } from './ContextMenu';
|
||||
import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from './ContextMenu';
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
import TimelinePanel from './TimelinePanel';
|
||||
import { Layout } from '../../settings/Layout';
|
||||
import { Layout } from '../../settings/enums/Layout';
|
||||
import { useEventEmitter } from '../../hooks/useEventEmitter';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import { TileShape } from '../views/rooms/EventTile';
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onClose: () => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
export enum ThreadFilterType {
|
||||
|
@ -69,46 +69,21 @@ const useFilteredThreadsTimelinePanel = ({
|
|||
pendingEvents: false,
|
||||
}), []);
|
||||
|
||||
useEffect(() => {
|
||||
let filteredThreads = Array.from(threads);
|
||||
if (filterOption === ThreadFilterType.My) {
|
||||
filteredThreads = filteredThreads.filter(([id, thread]) => {
|
||||
return thread.rootEvent.getSender() === userId;
|
||||
const buildThreadList = useCallback(function(timelineSet: EventTimelineSet) {
|
||||
timelineSet.resetLiveTimeline("");
|
||||
Array.from(threads)
|
||||
.forEach(([, thread]) => {
|
||||
if (filterOption !== ThreadFilterType.My || thread.hasCurrentUserParticipated) {
|
||||
timelineSet.addLiveEvent(thread.rootEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
// NOTE: Temporarily reverse the list until https://github.com/vector-im/element-web/issues/19393 gets properly resolved
|
||||
// The proper list order should be top-to-bottom, like in social-media newsfeeds.
|
||||
filteredThreads.reverse().forEach(([id, thread]) => {
|
||||
const event = thread.rootEvent;
|
||||
if (timelineSet.findEventById(event.getId()) || event.status !== null) return;
|
||||
timelineSet.addEventToTimeline(
|
||||
event,
|
||||
timelineSet.getLiveTimeline(),
|
||||
true,
|
||||
);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [room, timelineSet]);
|
||||
|
||||
useEventEmitter(room, ThreadEvent.Update, (thread) => {
|
||||
const event = thread.rootEvent;
|
||||
if (
|
||||
// If that's a reply and not an event
|
||||
event !== thread.replyToEvent &&
|
||||
timelineSet.findEventById(event.getId()) ||
|
||||
event.status !== null
|
||||
) return;
|
||||
if (event !== thread.events[thread.events.length - 1]) {
|
||||
timelineSet.removeEvent(thread.events[thread.events.length - 1]);
|
||||
timelineSet.removeEvent(event);
|
||||
}
|
||||
timelineSet.addEventToTimeline(
|
||||
event,
|
||||
timelineSet.getLiveTimeline(),
|
||||
false,
|
||||
);
|
||||
updateTimeline();
|
||||
});
|
||||
}, [filterOption, threads, updateTimeline]);
|
||||
|
||||
useEffect(() => { buildThreadList(timelineSet); }, [timelineSet, buildThreadList]);
|
||||
|
||||
useEventEmitter(room, ThreadEvent.Update, () => { buildThreadList(timelineSet); });
|
||||
useEventEmitter(room, ThreadEvent.New, () => { buildThreadList(timelineSet); });
|
||||
|
||||
return timelineSet;
|
||||
};
|
||||
|
@ -122,14 +97,14 @@ export const ThreadPanelHeaderFilterOptionItem = ({
|
|||
onClick: () => void;
|
||||
isSelected: boolean;
|
||||
}) => {
|
||||
return <AccessibleButton
|
||||
aria-selected={isSelected}
|
||||
return <MenuItemRadio
|
||||
active={isSelected}
|
||||
className="mx_ThreadPanel_Header_FilterOptionItem"
|
||||
onClick={onClick}
|
||||
>
|
||||
<span>{ label }</span>
|
||||
<span>{ description }</span>
|
||||
</AccessibleButton>;
|
||||
</MenuItemRadio>;
|
||||
};
|
||||
|
||||
export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
|
||||
|
@ -140,7 +115,7 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
|
|||
const options: readonly ThreadPanelHeaderOption[] = [
|
||||
{
|
||||
label: _t("My threads"),
|
||||
description: _t("Shows all threads you’ve participated in"),
|
||||
description: _t("Shows all threads you've participated in"),
|
||||
key: ThreadFilterType.My,
|
||||
},
|
||||
{
|
||||
|
@ -161,19 +136,49 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
|
|||
}}
|
||||
isSelected={opt === value}
|
||||
/>);
|
||||
const contextMenu = menuDisplayed ? <ContextMenu top={0} right={25} onFinished={closeMenu} managed={false}>
|
||||
const contextMenu = menuDisplayed ? <ContextMenu
|
||||
top={0}
|
||||
right={25}
|
||||
onFinished={closeMenu}
|
||||
chevronFace={ChevronFace.Top}
|
||||
mountAsChild={true}
|
||||
>
|
||||
{ contextMenuOptions }
|
||||
</ContextMenu> : null;
|
||||
return <div className="mx_ThreadPanel__header">
|
||||
<span>{ _t("Threads") }</span>
|
||||
<ContextMenuButton inputRef={button} isExpanded={menuDisplayed} onClick={() => menuDisplayed ? closeMenu() : openMenu()}>
|
||||
<ContextMenuButton className="mx_ThreadPanel_dropdown" inputRef={button} isExpanded={menuDisplayed} onClick={() => menuDisplayed ? closeMenu() : openMenu()}>
|
||||
{ `${_t('Show:')} ${value.label}` }
|
||||
</ContextMenuButton>
|
||||
{ contextMenu }
|
||||
</div>;
|
||||
};
|
||||
|
||||
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
|
||||
interface EmptyThreadIProps {
|
||||
filterOption: ThreadFilterType;
|
||||
showAllThreadsCallback: () => void;
|
||||
}
|
||||
|
||||
const EmptyThread: React.FC<EmptyThreadIProps> = ({ filterOption, showAllThreadsCallback }) => {
|
||||
return <aside className="mx_ThreadPanel_empty">
|
||||
<div className="mx_ThreadPanel_largeIcon" />
|
||||
<h2>{ _t("Keep discussions organised with threads") }</h2>
|
||||
<p>{ _t("Threads help you keep conversations on-topic and easily "
|
||||
+ "track them over time. Create the first one by using the "
|
||||
+ "\"Reply in thread\" button on a message.") }
|
||||
</p>
|
||||
<p>
|
||||
{ /* Always display that paragraph to prevent layout shift
|
||||
When hiding the button */ }
|
||||
{ filterOption === ThreadFilterType.My
|
||||
? <button onClick={showAllThreadsCallback}>{ _t("Show all threads") }</button>
|
||||
: <> </>
|
||||
}
|
||||
</p>
|
||||
</aside>;
|
||||
};
|
||||
|
||||
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => {
|
||||
const mxClient = useContext(MatrixClientContext);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const room = mxClient.getRoom(roomId);
|
||||
|
@ -199,7 +204,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
|
|||
header={<ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />}
|
||||
className="mx_ThreadPanel"
|
||||
onClose={onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
<TimelinePanel
|
||||
ref={ref}
|
||||
|
@ -209,7 +214,10 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
|
|||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={filteredTimelineSet}
|
||||
showUrlPreview={true}
|
||||
empty={<div>empty</div>}
|
||||
empty={<EmptyThread
|
||||
filterOption={filterOption}
|
||||
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)}
|
||||
/>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
|
@ -217,7 +225,9 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
|
|||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
membersLoaded={true}
|
||||
permalinkCreator={permalinkCreator}
|
||||
tileShape={TileShape.ThreadPanel}
|
||||
disableGrouping={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
||||
import { IEventRelation, MatrixEvent, Room } from 'matrix-js-sdk/src';
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
|
@ -26,7 +26,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
|
|||
import { TileShape } from '../views/rooms/EventTile';
|
||||
import MessageComposer from '../views/rooms/MessageComposer';
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import { Layout } from '../../settings/Layout';
|
||||
import { Layout } from '../../settings/enums/Layout';
|
||||
import TimelinePanel from './TimelinePanel';
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from '../../dispatcher/payloads';
|
||||
|
@ -36,6 +36,13 @@ import { MatrixClientPeg } from '../../MatrixClientPeg';
|
|||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import UploadBar from './UploadBar';
|
||||
import { _t } from '../../languageHandler';
|
||||
import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu';
|
||||
import RightPanelStore from '../../stores/RightPanelStore';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import { WidgetLayoutStore } from '../../stores/widgets/WidgetLayoutStore';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -47,7 +54,6 @@ interface IProps {
|
|||
initialEvent?: MatrixEvent;
|
||||
initialEventHighlighted?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
|
@ -78,7 +84,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
this.teardownThread();
|
||||
dis.unregister(this.dispatcherRef);
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
room.on(ThreadEvent.New, this.onNewThread);
|
||||
room.removeListener(ThreadEvent.New, this.onNewThread);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps) {
|
||||
|
@ -97,10 +103,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
if (payload.phase == RightPanelPhases.ThreadView && payload.event) {
|
||||
if (payload.event !== this.props.mxEvent) {
|
||||
this.teardownThread();
|
||||
this.setupThread(payload.event);
|
||||
}
|
||||
this.teardownThread();
|
||||
this.setupThread(payload.event);
|
||||
}
|
||||
switch (payload.action) {
|
||||
case Action.EditEvent:
|
||||
|
@ -129,15 +133,18 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private setupThread = (mxEv: MatrixEvent) => {
|
||||
let thread = mxEv.getThread();
|
||||
let thread = this.props.room.threads.get(mxEv.getId());
|
||||
if (!thread) {
|
||||
const client = MatrixClientPeg.get();
|
||||
// Do not attach this thread object to the event for now
|
||||
// TODO: When local echo gets reintroduced it will be important
|
||||
// to add that back in, and the threads model should go through the
|
||||
// same reconciliation algorithm as events
|
||||
thread = new Thread(
|
||||
[mxEv],
|
||||
this.props.room,
|
||||
client,
|
||||
);
|
||||
mxEv.setThread(thread);
|
||||
}
|
||||
thread.on(ThreadEvent.Update, this.updateThread);
|
||||
thread.once(ThreadEvent.Ready, this.updateThread);
|
||||
|
@ -159,10 +166,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private updateThread = (thread?: Thread) => {
|
||||
if (thread) {
|
||||
if (thread && this.state.thread !== thread) {
|
||||
this.setState({
|
||||
thread,
|
||||
});
|
||||
thread.emit(ThreadEvent.ViewThread);
|
||||
}
|
||||
|
||||
this.timelinePanelRef.current?.refreshTimeline();
|
||||
|
@ -171,7 +179,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
private onScroll = (): void => {
|
||||
if (this.props.initialEvent && this.props.initialEventHighlighted) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.props.room.roomId,
|
||||
event_id: this.props.initialEvent?.getId(),
|
||||
highlighted: false,
|
||||
|
@ -180,10 +188,43 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private renderThreadViewHeader = (): JSX.Element => {
|
||||
return <div className="mx_ThreadPanel__header">
|
||||
<span>{ _t("Thread") }</span>
|
||||
<ThreadListContextMenu
|
||||
mxEvent={this.props.mxEvent}
|
||||
permalinkCreator={this.props.permalinkCreator} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const highlightedEventId = this.props.initialEventHighlighted
|
||||
? this.props.initialEvent?.getId()
|
||||
: null;
|
||||
|
||||
const threadRelation: IEventRelation = {
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: this.state.thread?.id,
|
||||
};
|
||||
|
||||
let previousPhase = RightPanelStore.getSharedInstance().previousPhase;
|
||||
if (!SettingsStore.getValue("feature_maximised_widgets")) {
|
||||
previousPhase = RightPanelPhases.ThreadPanel;
|
||||
}
|
||||
|
||||
// change the previous phase to the threadPanel in case there is no maximised widget anymore
|
||||
if (!WidgetLayoutStore.instance.hasMaximisedWidget(this.props.room)) {
|
||||
previousPhase = RightPanelPhases.ThreadPanel;
|
||||
}
|
||||
|
||||
// Make sure the previous Phase is always one of the two: Timeline or ThreadPanel
|
||||
if (![RightPanelPhases.ThreadPanel, RightPanelPhases.Timeline].includes(previousPhase)) {
|
||||
previousPhase = RightPanelPhases.ThreadPanel;
|
||||
}
|
||||
const previousPhaseLabels = {};
|
||||
previousPhaseLabels[RightPanelPhases.ThreadPanel] = _t("All threads");
|
||||
previousPhaseLabels[RightPanelPhases.Timeline] = _t("Chat");
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
|
@ -192,23 +233,24 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}}>
|
||||
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
className="mx_ThreadView mx_ThreadPanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.ThreadPanel}
|
||||
previousPhase={previousPhase}
|
||||
previousPhaseLabel={previousPhaseLabels[previousPhase]}
|
||||
withoutScrollContainer={true}
|
||||
header={this.renderThreadViewHeader()}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
showReadReceipts={false} // Hide the read receipts
|
||||
// until homeservers speak threads language
|
||||
manageReadReceipts={true}
|
||||
manageReadMarkers={true}
|
||||
sendReadReceiptOnLoad={true}
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
|
@ -223,13 +265,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
/>
|
||||
) }
|
||||
|
||||
{ ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && (
|
||||
<UploadBar room={this.props.room} relation={threadRelation} />
|
||||
) }
|
||||
|
||||
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
relation={{
|
||||
rel_type: RelationType.Thread,
|
||||
event_id: this.state.thread.id,
|
||||
}}
|
||||
relation={threadRelation}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
|
|
|
@ -27,13 +27,14 @@ import { debounce } from 'lodash';
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import { _t } from '../../languageHandler';
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import UserActivity from "../../UserActivity";
|
||||
import Modal from "../../Modal";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
import { Key } from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
|
@ -132,6 +133,7 @@ interface IProps {
|
|||
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>;
|
||||
|
||||
hideThreadedMessages?: boolean;
|
||||
disableGrouping?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -221,6 +223,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
className: 'mx_RoomView_messagePanel',
|
||||
sendReadReceiptOnLoad: true,
|
||||
hideThreadedMessages: true,
|
||||
disableGrouping: false,
|
||||
};
|
||||
|
||||
private lastRRSentEventId: string = undefined;
|
||||
|
@ -474,10 +477,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onMessageListScroll = e => {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(e);
|
||||
}
|
||||
|
||||
this.props.onScroll?.(e);
|
||||
if (this.props.manageReadMarkers) {
|
||||
this.doManageReadMarkers();
|
||||
}
|
||||
|
@ -505,7 +505,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
// (and user is active), switch timeout
|
||||
const timeout = this.readMarkerTimeout(rmPosition);
|
||||
// NO-OP when timeout already has set to the given value
|
||||
this.readMarkerActivityTimer.changeTimeout(timeout);
|
||||
this.readMarkerActivityTimer?.changeTimeout(timeout);
|
||||
}, READ_MARKER_DEBOUNCE_MS, { leading: false, trailing: true });
|
||||
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
|
@ -592,7 +592,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
this.setState<null>(updatedState, () => {
|
||||
this.messagePanel.current.updateTimelineMinHeight();
|
||||
if (callRMUpdated) {
|
||||
this.props.onReadMarkerUpdated();
|
||||
this.props.onReadMarkerUpdated?.();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1134,7 +1134,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
onFinished = () => {
|
||||
// go via the dispatcher so that the URL is updated
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.props.timelineSet.room.roomId,
|
||||
});
|
||||
};
|
||||
|
@ -1309,12 +1309,17 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private indexForEventId(evId: string): number | null {
|
||||
for (let i = 0; i < this.state.events.length; ++i) {
|
||||
if (evId == this.state.events[i].getId()) {
|
||||
return i;
|
||||
}
|
||||
/* Threads do not have server side support for read receipts and the concept
|
||||
is very tied to the main room timeline, we are forcing the timeline to
|
||||
send read receipts for threaded events */
|
||||
const isThreadTimeline = this.context.timelineRenderingType === TimelineRenderingType.Thread;
|
||||
if (SettingsStore.getValue("feature_thread") && isThreadTimeline) {
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
const index = this.state.events.findIndex(ev => ev.getId() === evId);
|
||||
return index > -1
|
||||
? index
|
||||
: null;
|
||||
}
|
||||
|
||||
private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null {
|
||||
|
@ -1538,6 +1543,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
hideThreadedMessages={this.props.hideThreadedMessages}
|
||||
disableGrouping={this.props.disableGrouping}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,9 +28,11 @@ import AccessibleButton from "../views/elements/AccessibleButton";
|
|||
import { IUpload } from "../../models/IUpload";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { IEventRelation } from 'matrix-js-sdk/src';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
relation?: IEventRelation;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -65,7 +67,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private getUploadsInRoom(): IUpload[] {
|
||||
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
|
||||
const uploads = ContentMessages.sharedInstance().getCurrentUploads(this.props.relation);
|
||||
return uploads.filter(u => u.roomId === this.props.room.roomId);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import React, { createRef, useContext, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
@ -26,7 +25,7 @@ import dis from "../../dispatcher/dispatcher";
|
|||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { ContextMenuButton } from "./ContextMenu";
|
||||
import { ChevronFace, ContextMenuButton } from "./ContextMenu";
|
||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||
|
@ -43,26 +42,62 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
|
|||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuCheckbox,
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||
import { showCommunityInviteDialog } from "../../RoomInvite";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
|
||||
import { UIFeature } from "../../settings/UIFeature";
|
||||
import HostSignupAction from "./HostSignupAction";
|
||||
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import SpaceStore from "../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import TooltipButton from "../views/elements/TooltipButton";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
|
||||
|
||||
const CustomStatusSection = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const setStatus = cli.getUser(cli.getUserId()).unstable_statusMessage || "";
|
||||
const [value, setValue] = useState(setStatus);
|
||||
|
||||
let details: JSX.Element;
|
||||
if (value !== setStatus) {
|
||||
details = <>
|
||||
<p>{ _t("Your status will be shown to people you have a DM with.") }</p>
|
||||
|
||||
<AccessibleButton
|
||||
onClick={() => cli._unstable_setStatusMessage(value)}
|
||||
kind="primary_outline"
|
||||
>
|
||||
{ value ? _t("Set status") : _t("Clear status") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <div className="mx_UserMenu_CustomStatusSection">
|
||||
<div className="mx_UserMenu_CustomStatusSection_input">
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
placeholder={_t("Set a new status")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<AccessibleButton
|
||||
tabIndex={-1}
|
||||
title={_t("Clear")}
|
||||
className="mx_UserMenu_CustomStatusSection_clear"
|
||||
onClick={() => setValue("")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ details }
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
isPanelCollapsed: boolean;
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
|
||||
|
@ -72,14 +107,30 @@ interface IState {
|
|||
isDarkTheme: boolean;
|
||||
isHighContrast: boolean;
|
||||
selectedSpace?: Room;
|
||||
pendingRoomJoin: Set<string>;
|
||||
dndEnabled: boolean;
|
||||
}
|
||||
|
||||
const toRightOf = (rect: PartialDOMRect) => {
|
||||
return {
|
||||
left: rect.width + rect.left + 8,
|
||||
top: rect.top,
|
||||
chevronFace: ChevronFace.None,
|
||||
};
|
||||
};
|
||||
|
||||
const below = (rect: PartialDOMRect) => {
|
||||
return {
|
||||
left: rect.left,
|
||||
top: rect.top + rect.height,
|
||||
chevronFace: ChevronFace.None,
|
||||
};
|
||||
};
|
||||
|
||||
@replaceableComponent("structures.UserMenu")
|
||||
export default class UserMenu extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private themeWatcherRef: string;
|
||||
private dndWatcherRef: string;
|
||||
private readonly dndWatcherRef: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
private tagStoreRef: fbEmitter.EventSubscription;
|
||||
|
||||
|
@ -90,7 +141,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
contextMenuPosition: null,
|
||||
isDarkTheme: this.isUserOnDarkTheme(),
|
||||
isHighContrast: this.isUserOnHighContrastTheme(),
|
||||
pendingRoomJoin: new Set<string>(),
|
||||
dndEnabled: this.doNotDisturb,
|
||||
selectedSpace: SpaceStore.instance.activeSpaceRoom,
|
||||
};
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
|
@ -98,8 +150,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||
}
|
||||
|
||||
// Force update is the easiest way to trigger the UI update (we don't store state for this)
|
||||
this.dndWatcherRef = SettingsStore.watchSetting("doNotDisturb", null, () => this.forceUpdate());
|
||||
SettingsStore.monitorSetting("feature_dnd", null);
|
||||
SettingsStore.monitorSetting("doNotDisturb", null);
|
||||
}
|
||||
|
||||
private get doNotDisturb(): boolean {
|
||||
return SettingsStore.getValue("doNotDisturb");
|
||||
}
|
||||
|
||||
private get hasHomePage(): boolean {
|
||||
|
@ -110,7 +166,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
|
||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -122,13 +177,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
if (SpaceStore.spacesEnabled) {
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
|
||||
}
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
}
|
||||
|
||||
private onRoom = (room: Room): void => {
|
||||
this.removePendingJoinRoom(room.roomId);
|
||||
};
|
||||
|
||||
private onTagStoreUpdate = () => {
|
||||
this.forceUpdate(); // we don't have anything useful in state to update
|
||||
};
|
||||
|
@ -163,8 +213,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onSelectedSpaceUpdate = async (selectedSpace?: Room) => {
|
||||
this.setState({ selectedSpace });
|
||||
private onSelectedSpaceUpdate = async () => {
|
||||
this.setState({
|
||||
selectedSpace: SpaceStore.instance.activeSpaceRoom,
|
||||
});
|
||||
};
|
||||
|
||||
private onThemeChanged = () => {
|
||||
|
@ -175,8 +227,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onAction = (ev: ActionPayload) => {
|
||||
switch (ev.action) {
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
case Action.ToggleUserMenu:
|
||||
if (this.state.contextMenuPosition) {
|
||||
this.setState({ contextMenuPosition: null });
|
||||
|
@ -184,36 +236,27 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||
}
|
||||
break;
|
||||
case Action.JoinRoom:
|
||||
this.addPendingJoinRoom(ev.roomId);
|
||||
break;
|
||||
case Action.JoinRoomReady:
|
||||
case Action.JoinRoomError:
|
||||
this.removePendingJoinRoom(ev.roomId);
|
||||
break;
|
||||
|
||||
case Action.SettingUpdated: {
|
||||
const settingUpdatedPayload = payload as SettingUpdatedPayload;
|
||||
switch (settingUpdatedPayload.settingName) {
|
||||
case "feature_dnd":
|
||||
case "doNotDisturb": {
|
||||
const dndEnabled = this.doNotDisturb;
|
||||
if (this.state.dndEnabled !== dndEnabled) {
|
||||
this.setState({ dndEnabled });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private addPendingJoinRoom(roomId: string): void {
|
||||
this.setState({
|
||||
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin)
|
||||
.add(roomId),
|
||||
});
|
||||
}
|
||||
|
||||
private removePendingJoinRoom(roomId: string): void {
|
||||
if (this.state.pendingRoomJoin.delete(roomId)) {
|
||||
this.setState({
|
||||
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
|
||||
this.setState({ contextMenuPosition: ev.currentTarget.getBoundingClientRect() });
|
||||
};
|
||||
|
||||
private onContextMenu = (ev: React.MouseEvent) => {
|
||||
|
@ -259,15 +302,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onShowArchived = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038
|
||||
// Note: You'll need to uncomment the button too.
|
||||
logger.log("TODO: Show archived rooms");
|
||||
};
|
||||
|
||||
private onProvideFeedback = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -309,50 +343,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onCommunitySettingsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
|
||||
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
|
||||
});
|
||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onCommunityMembersClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// We'd ideally just pop open a right panel with the member list, but the current
|
||||
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
|
||||
// switch to the general room and open the member list there as it should be in sync
|
||||
// anyways.
|
||||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: chat.roomId,
|
||||
}, true);
|
||||
dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList });
|
||||
} else {
|
||||
// "This should never happen" clauses go here for the prototype.
|
||||
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
|
||||
title: _t('Failed to find the general chat for this community'),
|
||||
description: _t("Failed to find the general chat for this community"),
|
||||
});
|
||||
}
|
||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onCommunityInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||
};
|
||||
|
||||
private onDndToggle = (ev) => {
|
||||
private onDndToggle = (ev: ButtonEvent) => {
|
||||
ev.stopPropagation();
|
||||
const current = SettingsStore.getValue("doNotDisturb");
|
||||
SettingsStore.setValue("doNotDisturb", null, SettingLevel.DEVICE, !current);
|
||||
|
@ -361,8 +352,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
private renderContextMenu = (): React.ReactNode => {
|
||||
if (!this.state.contextMenuPosition) return null;
|
||||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let topSection;
|
||||
const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup;
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
|
@ -408,6 +397,24 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let customStatusSection: JSX.Element;
|
||||
if (SettingsStore.getValue("feature_custom_status")) {
|
||||
customStatusSection = <CustomStatusSection />;
|
||||
}
|
||||
|
||||
let dndButton: JSX.Element;
|
||||
if (SettingsStore.getValue("feature_dnd")) {
|
||||
dndButton = (
|
||||
<IconizedContextMenuCheckbox
|
||||
iconClassName={this.state.dndEnabled ? "mx_UserMenu_iconDnd" : "mx_UserMenu_iconDndOff"}
|
||||
label={_t("Do not disturb")}
|
||||
onClick={this.onDndToggle}
|
||||
active={this.state.dndEnabled}
|
||||
words
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let feedbackButton;
|
||||
if (SettingsStore.getValue(UIFeature.Feedback)) {
|
||||
feedbackButton = <IconizedContextMenuOption
|
||||
|
@ -417,156 +424,68 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>;
|
||||
}
|
||||
|
||||
let primaryHeader = (
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{ OwnProfileStore.instance.displayName }
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{ MatrixClientPeg.get().getUserId() }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
let primaryOptionList = (
|
||||
<React.Fragment>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ homeButton }
|
||||
{ dndButton }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("Notifications")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & privacy")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("All settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
<IconizedContextMenuOption
|
||||
className="mx_IconizedContextMenu_option_red"
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
primaryOptionList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ homeButton }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("Notification settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & privacy")}
|
||||
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("All settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ /* <IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconArchive"
|
||||
label={_t("Archived rooms")}
|
||||
onClick={this.onShowArchived}
|
||||
/> */ }
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
let secondarySection = null;
|
||||
|
||||
if (prototypeCommunityName) {
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
primaryHeader = (
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{ prototypeCommunityName }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
let settingsOption;
|
||||
let inviteOption;
|
||||
if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconInvite"
|
||||
label={_t("Invite")}
|
||||
onClick={this.onCommunityInviteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("Community settings")}
|
||||
onClick={this.onCommunitySettingsClick}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
primaryOptionList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ settingsOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={this.onCommunityMembersClick}
|
||||
/>
|
||||
{ inviteOption }
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
secondarySection = (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{ OwnProfileStore.instance.displayName }
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{ MatrixClientPeg.get().getUserId() }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("User settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else if (MatrixClientPeg.get().isGuest()) {
|
||||
primaryOptionList = (
|
||||
<React.Fragment>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ homeButton }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_UserMenu_contextMenu": true,
|
||||
"mx_UserMenu_contextMenu_prototype": !!prototypeCommunityName,
|
||||
});
|
||||
const position = this.props.isPanelCollapsed
|
||||
? toRightOf(this.state.contextMenuPosition)
|
||||
: below(this.state.contextMenuPosition);
|
||||
|
||||
return <IconizedContextMenu
|
||||
// numerical adjustments to overlap the context menu by just over the width of the
|
||||
// menu icon and make it look connected
|
||||
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 10}
|
||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height + 8}
|
||||
{...position}
|
||||
onFinished={this.onCloseMenu}
|
||||
className={classes}
|
||||
className="mx_UserMenu_contextMenu"
|
||||
>
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
{ primaryHeader }
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{ OwnProfileStore.instance.displayName }
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{ MatrixClientPeg.get().getUserId() }
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AccessibleTooltipButton
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
|
@ -579,9 +498,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
{ customStatusSection }
|
||||
{ topSection }
|
||||
{ primaryOptionList }
|
||||
{ secondarySection }
|
||||
</IconizedContextMenu>;
|
||||
};
|
||||
|
||||
|
@ -592,102 +511,47 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
const displayName = OwnProfileStore.instance.displayName || userId;
|
||||
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
||||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
let badge: JSX.Element;
|
||||
if (this.state.dndEnabled) {
|
||||
badge = <div className="mx_UserMenu_dndBadge" />;
|
||||
}
|
||||
|
||||
let isPrototype = false;
|
||||
let menuName = _t("User menu");
|
||||
let name = <span className="mx_UserMenu_userName">{ displayName }</span>;
|
||||
let buttons = (
|
||||
<span className="mx_UserMenu_headerButtons">
|
||||
{ /* masked image in CSS */ }
|
||||
</span>
|
||||
);
|
||||
let dnd;
|
||||
if (this.state.selectedSpace) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{ displayName }</span>
|
||||
<RoomName room={this.state.selectedSpace}>
|
||||
{ (roomName) => <span className="mx_UserMenu_subUserName">{ roomName }</span> }
|
||||
</RoomName>
|
||||
</div>
|
||||
);
|
||||
} else if (prototypeCommunityName) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{ prototypeCommunityName }</span>
|
||||
<span className="mx_UserMenu_subUserName">{ displayName }</span>
|
||||
</div>
|
||||
);
|
||||
menuName = _t("Community and user menu");
|
||||
isPrototype = true;
|
||||
} else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{ _t("Home") }</span>
|
||||
<span className="mx_UserMenu_subUserName">{ displayName }</span>
|
||||
</div>
|
||||
);
|
||||
isPrototype = true;
|
||||
} else if (SettingsStore.getValue("feature_dnd")) {
|
||||
const isDnd = SettingsStore.getValue("doNotDisturb");
|
||||
dnd = <AccessibleButton
|
||||
onClick={this.onDndToggle}
|
||||
let name: JSX.Element;
|
||||
if (!this.props.isPanelCollapsed) {
|
||||
name = <div className="mx_UserMenu_name">
|
||||
{ displayName }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_UserMenu">
|
||||
<ContextMenuButton
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={_t("User menu")}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
onContextMenu={this.onContextMenu}
|
||||
className={classNames({
|
||||
"mx_UserMenu_dnd": true,
|
||||
"mx_UserMenu_dnd_noisy": !isDnd,
|
||||
"mx_UserMenu_dnd_muted": isDnd,
|
||||
mx_UserMenu_cutout: badge,
|
||||
})}
|
||||
/>;
|
||||
}
|
||||
if (this.props.isMinimized) {
|
||||
name = null;
|
||||
buttons = null;
|
||||
}
|
||||
>
|
||||
<div className="mx_UserMenu_userAvatar">
|
||||
<BaseAvatar
|
||||
idName={userId}
|
||||
name={displayName}
|
||||
url={avatarUrl}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
resizeMethod="crop"
|
||||
className="mx_UserMenu_userAvatar_BaseAvatar"
|
||||
/>
|
||||
{ badge }
|
||||
</div>
|
||||
{ name }
|
||||
|
||||
const classes = classNames({
|
||||
'mx_UserMenu': true,
|
||||
'mx_UserMenu_minimized': this.props.isMinimized,
|
||||
'mx_UserMenu_prototype': isPrototype,
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={classes}
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={menuName}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_UserMenu_row">
|
||||
<span className="mx_UserMenu_userAvatarContainer">
|
||||
<BaseAvatar
|
||||
idName={userId}
|
||||
name={displayName}
|
||||
url={avatarUrl}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
resizeMethod="crop"
|
||||
className="mx_UserMenu_userAvatar"
|
||||
/>
|
||||
</span>
|
||||
{ name }
|
||||
{ this.state.pendingRoomJoin.size > 0 && (
|
||||
<InlineSpinner>
|
||||
<TooltipButton helpText={_t(
|
||||
"Currently joining %(count)s rooms",
|
||||
{ count: this.state.pendingRoomJoin.size },
|
||||
)} />
|
||||
</InlineSpinner>
|
||||
) }
|
||||
{ dnd }
|
||||
{ buttons }
|
||||
</div>
|
||||
</ContextMenuButton>
|
||||
{ this.renderContextMenu() }
|
||||
</React.Fragment>
|
||||
);
|
||||
</ContextMenuButton>
|
||||
|
||||
{ this.props.children }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
|
@ -59,8 +60,6 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||
const { phase, lostKeys } = this.state;
|
||||
let icon;
|
||||
let title;
|
||||
|
|
|
@ -17,14 +17,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from 'classnames';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import ServerPicker from "../../views/elements/ServerPicker";
|
||||
|
@ -32,8 +29,14 @@ import EmailField from "../../views/auth/EmailField";
|
|||
import PassphraseField from '../../views/auth/PassphraseField';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
|
||||
import { IValidationResult } from "../../views/elements/Validation";
|
||||
import InlineSpinner from '../../views/elements/InlineSpinner';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
import QuestionDialog from "../../views/dialogs/QuestionDialog";
|
||||
import ErrorDialog from "../../views/dialogs/ErrorDialog";
|
||||
import AuthHeader from "../../views/auth/AuthHeader";
|
||||
import AuthBody from "../../views/auth/AuthBody";
|
||||
import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField";
|
||||
|
||||
enum Phase {
|
||||
// Show the forgot password inputs
|
||||
|
@ -68,11 +71,15 @@ interface IState {
|
|||
serverErrorIsFatal: boolean;
|
||||
serverDeadError: string;
|
||||
|
||||
emailFieldValid: boolean;
|
||||
passwordFieldValid: boolean;
|
||||
currentHttpRequest?: Promise<any>;
|
||||
}
|
||||
|
||||
enum ForgotPasswordField {
|
||||
Email = 'field_email',
|
||||
Password = 'field_password',
|
||||
PasswordConfirm = 'field_password_confirm',
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.ForgotPassword")
|
||||
export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||
private reset: PasswordReset;
|
||||
|
@ -91,8 +98,6 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
emailFieldValid: false,
|
||||
passwordFieldValid: false,
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
|
@ -171,42 +176,58 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
// refresh the server errors, just in case the server came back online
|
||||
await this.handleHttpRequest(this.checkServerLiveliness(this.props.serverConfig));
|
||||
|
||||
await this['email_field'].validate({ allowEmpty: false });
|
||||
await this['password_field'].validate({ allowEmpty: false });
|
||||
|
||||
if (!this.state.email) {
|
||||
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||
} else if (!this.state.emailFieldValid) {
|
||||
this.showErrorDialog(_t("The email address doesn't appear to be valid."));
|
||||
} else if (!this.state.password || !this.state.password2) {
|
||||
this.showErrorDialog(_t('A new password must be entered.'));
|
||||
} else if (!this.state.passwordFieldValid) {
|
||||
this.showErrorDialog(_t('Please choose a strong password'));
|
||||
} else if (this.state.password !== this.state.password2) {
|
||||
this.showErrorDialog(_t('New passwords must match each other.'));
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description:
|
||||
<div>
|
||||
{ _t(
|
||||
"Changing your password will reset any end-to-end encryption keys " +
|
||||
"on all of your sessions, making encrypted chat history unreadable. Set up " +
|
||||
"Key Backup or export your room keys from another session before resetting your " +
|
||||
"password.",
|
||||
) }
|
||||
</div>,
|
||||
button: _t('Continue'),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(this.state.email, this.state.password);
|
||||
}
|
||||
},
|
||||
});
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description:
|
||||
<div>
|
||||
{ _t(
|
||||
"Changing your password will reset any end-to-end encryption keys " +
|
||||
"on all of your sessions, making encrypted chat history unreadable. Set up " +
|
||||
"Key Backup or export your room keys from another session before resetting your " +
|
||||
"password.",
|
||||
) }
|
||||
</div>,
|
||||
button: _t('Continue'),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(this.state.email, this.state.password);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private async verifyFieldsBeforeSubmit() {
|
||||
const fieldIdsInDisplayOrder = [
|
||||
ForgotPasswordField.Email,
|
||||
ForgotPasswordField.Password,
|
||||
ForgotPasswordField.PasswordConfirm,
|
||||
];
|
||||
|
||||
const invalidFields = [];
|
||||
for (const fieldId of fieldIdsInDisplayOrder) {
|
||||
const valid = await this[fieldId].validate({ allowEmpty: false });
|
||||
if (!valid) {
|
||||
invalidFields.push(this[fieldId]);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidFields.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Focus on the first invalid field, then re-validate,
|
||||
// which will result in the error tooltip being displayed for that field.
|
||||
invalidFields[0].focus();
|
||||
invalidFields[0].validate({ allowEmpty: false, focused: true });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private onInputChanged = (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
[stateKey]: ev.currentTarget.value,
|
||||
|
@ -220,25 +241,12 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public showErrorDialog(description: string, title?: string) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
|
||||
title,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
private onEmailValidate = (result: IValidationResult) => {
|
||||
this.setState({
|
||||
emailFieldValid: result.valid,
|
||||
});
|
||||
};
|
||||
|
||||
private onPasswordValidate(result: IValidationResult) {
|
||||
this.setState({
|
||||
passwordFieldValid: result.valid,
|
||||
});
|
||||
}
|
||||
|
||||
private handleHttpRequest<T = unknown>(request: Promise<T>): Promise<T> {
|
||||
this.setState({
|
||||
currentHttpRequest: request,
|
||||
|
@ -251,8 +259,6 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
renderForgot() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
let errorText = null;
|
||||
const err = this.state.errorText;
|
||||
if (err) {
|
||||
|
@ -284,11 +290,12 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
<div className="mx_AuthBody_fieldRow">
|
||||
<EmailField
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
labelRequired={_td('The email address linked to your account must be entered.')}
|
||||
labelInvalid={_td("The email address doesn't appear to be valid.")}
|
||||
value={this.state.email}
|
||||
fieldRef={field => this['email_field'] = field}
|
||||
fieldRef={field => this[ForgotPasswordField.Email] = field}
|
||||
autoFocus={true}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
onValidate={this.onEmailValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")}
|
||||
/>
|
||||
|
@ -300,18 +307,20 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
label={_td('New Password')}
|
||||
value={this.state.password}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
fieldRef={field => this[ForgotPasswordField.Password] = field}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
fieldRef={field => this['password_field'] = field}
|
||||
onValidate={(result) => this.onPasswordValidate(result)}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Field
|
||||
<PassphraseConfirmField
|
||||
name="reset_password_confirm"
|
||||
type="password"
|
||||
label={_t('Confirm')}
|
||||
label={_td('Confirm')}
|
||||
labelRequired={_td("A new password must be entered.")}
|
||||
labelInvalid={_td("New passwords must match each other.")}
|
||||
value={this.state.password2}
|
||||
password={this.state.password}
|
||||
fieldRef={field => this[ForgotPasswordField.PasswordConfirm] = field}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
|
||||
|
@ -335,7 +344,6 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
renderSendingEmail() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
@ -372,9 +380,6 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
|
||||
let resetPasswordJsx;
|
||||
switch (this.state.phase) {
|
||||
case Phase.Forgot:
|
||||
|
|
|
@ -303,7 +303,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
errorText = _t('This server does not support authentication with a phone number.');
|
||||
}
|
||||
} else if (response.errcode === "M_USER_IN_USE") {
|
||||
errorText = _t("That username already exists, please try another.");
|
||||
errorText = _t("Someone already has that username, please try another.");
|
||||
} else if (response.errcode === "M_THREEPID_IN_USE") {
|
||||
errorText = _t("That e-mail address is already in use.");
|
||||
}
|
||||
|
@ -509,6 +509,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
flows={this.state.flows}
|
||||
serverConfig={this.props.serverConfig}
|
||||
canSubmit={!this.state.serverErrorIsFatal}
|
||||
matrixClient={this.state.matrixClient}
|
||||
/>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean {
|
|||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished: (boolean) => void;
|
||||
onFinished: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -70,7 +70,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
private onStoreUpdate = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
if (store.phase === Phase.Finished) {
|
||||
this.props.onFinished(true);
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
|
@ -97,13 +97,16 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
const userId = cli.getUserId();
|
||||
const requestPromise = cli.requestVerification(userId);
|
||||
|
||||
this.props.onFinished(true);
|
||||
// We need to call onFinished now to close this dialog, and
|
||||
// again later to signal that the verification is complete.
|
||||
this.props.onFinished();
|
||||
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||
verificationRequestPromise: requestPromise,
|
||||
member: cli.getUser(userId),
|
||||
onFinished: async () => {
|
||||
const request = await requestPromise;
|
||||
request.cancel();
|
||||
this.props.onFinished();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -125,6 +128,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private onResetConfirmClick = () => {
|
||||
this.props.onFinished();
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.resetConfirm();
|
||||
};
|
||||
|
@ -140,7 +144,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private onEncryptionPanelClose = () => {
|
||||
this.props.onFinished(false);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
@ -249,7 +253,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Without verifying, you won’t have access to all your messages " +
|
||||
"Without verifying, you won't have access to all your messages " +
|
||||
"and may appear as untrusted to others.",
|
||||
) }</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
|
|
|
@ -215,7 +215,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
if (this.state.keyBackupNeeded) {
|
||||
introText = _t(
|
||||
"Regain access to your account and recover encryption keys stored in this session. " +
|
||||
"Without them, you won’t be able to read all of your secure messages in any session.");
|
||||
"Without them, you won't be able to read all of your secure messages in any session.");
|
||||
}
|
||||
|
||||
if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue