Merge branch 'develop' into improve-file-drop-ui
This commit is contained in:
commit
c1a105f071
243 changed files with 17703 additions and 6293 deletions
|
@ -397,7 +397,8 @@ export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
|||
return {left, top, chevronOffset};
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
|
||||
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
|
||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
|
@ -416,6 +417,41 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
|||
return menuOptions;
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
// and always above elementRect
|
||||
export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonRight = elementRect.right + window.pageXOffset;
|
||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||
const buttonTop = elementRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the right edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
menuOptions.top = buttonBottom + vPadding;
|
||||
} else {
|
||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||
}
|
||||
|
||||
return menuOptions;
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the right of elementRect
|
||||
// and always above elementRect
|
||||
export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonLeft = elementRect.left + window.pageXOffset;
|
||||
const buttonTop = elementRect.top + window.pageYOffset;
|
||||
// Align the left edge of the menu to the left edge of the button
|
||||
menuOptions.left = buttonLeft;
|
||||
// Align the menu vertically above the menu
|
||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
||||
|
||||
return menuOptions;
|
||||
};
|
||||
|
||||
type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val: boolean) => void];
|
||||
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
|
||||
const button = useRef<T>(null);
|
||||
|
|
|
@ -45,7 +45,7 @@ class FilePanel extends React.Component {
|
|||
};
|
||||
|
||||
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
|
||||
if (room.roomId !== this.props.roomId) return;
|
||||
if (room?.roomId !== this.props?.roomId) return;
|
||||
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||
|
||||
if (ev.isBeingDecrypted()) {
|
||||
|
|
56
src/components/structures/HostSignupAction.tsx
Normal file
56
src/components/structures/HostSignupAction.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { HostSignupStore } from "../../stores/HostSignupStore";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {}
|
||||
|
||||
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
||||
private openDialog = async () => {
|
||||
await HostSignupStore.instance.setHostSignupActive(true);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const hostSignupConfig = SdkConfig.get().hostSignup;
|
||||
if (!hostSignupConfig?.brand) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconHosting"
|
||||
label={_t(
|
||||
"Upgrade to %(hostSignupBrand)s",
|
||||
{
|
||||
hostSignupBrand: hostSignupConfig.brand,
|
||||
},
|
||||
)}
|
||||
onClick={this.openDialog}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -177,7 +177,14 @@ export default class InteractiveAuthComponent extends React.Component {
|
|||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
}, () => {
|
||||
if (oldStage != stageType) this._setFocus();
|
||||
if (oldStage !== stageType) {
|
||||
this._setFocus();
|
||||
} else if (
|
||||
!stageState.error && this._stageComponent.current &&
|
||||
this._stageComponent.current.attemptFailed
|
||||
) {
|
||||
this._stageComponent.current.attemptFailed();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
|||
|
||||
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
||||
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
||||
useEffect(onResize, [expanded]);
|
||||
useEffect(onResize, [expanded, onResize]);
|
||||
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
|
|
@ -54,6 +54,7 @@ import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPa
|
|||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import Modal from "../../Modal";
|
||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
|
@ -140,7 +141,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
protected readonly _matrixClient: MatrixClient;
|
||||
protected readonly _roomView: React.RefObject<any>;
|
||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||
protected readonly _compactLayoutWatcherRef: string;
|
||||
protected compactLayoutWatcherRef: string;
|
||||
protected resizer: Resizer;
|
||||
|
||||
constructor(props, context) {
|
||||
|
@ -157,18 +158,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
CallMediaHandler.loadDevices();
|
||||
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
this._compactLayoutWatcherRef = SettingsStore.watchSetting(
|
||||
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||
);
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
this._roomView = React.createRef();
|
||||
|
@ -176,6 +165,24 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
// Call `onSync` with the current state as well
|
||||
this.onSync(
|
||||
this._matrixClient.getSyncState(),
|
||||
null,
|
||||
this._matrixClient.getSyncStateData(),
|
||||
);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
this.compactLayoutWatcherRef = SettingsStore.watchSetting(
|
||||
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||
);
|
||||
|
||||
this.resizer = this._createResizer();
|
||||
this.resizer.attach();
|
||||
this._loadResizerPreferences();
|
||||
|
@ -186,7 +193,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
SettingsStore.unwatchSetting(this._compactLayoutWatcherRef);
|
||||
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
|
||||
this.resizer.detach();
|
||||
}
|
||||
|
||||
|
@ -209,10 +216,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
_createResizer() {
|
||||
let size;
|
||||
let collapsed;
|
||||
const collapseConfig: ICollapseConfig = {
|
||||
toggleSize: 260 - 50,
|
||||
onCollapsed: (collapsed) => {
|
||||
if (collapsed) {
|
||||
onCollapsed: (_collapsed) => {
|
||||
collapsed = _collapsed;
|
||||
if (_collapsed) {
|
||||
dis.dispatch({action: "hide_left_panel"}, true);
|
||||
window.localStorage.setItem("mx_lhs_size", '0');
|
||||
} else {
|
||||
|
@ -227,7 +236,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.props.resizeNotifier.startResizing();
|
||||
},
|
||||
onResizeStop: () => {
|
||||
window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
this.props.resizeNotifier.stopResizing();
|
||||
},
|
||||
};
|
||||
|
@ -419,6 +428,14 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.F:
|
||||
if (ctrlCmdOnly && SettingsStore.getValue("ctrlFForSearch")) {
|
||||
dis.dispatch({
|
||||
action: 'focus_search',
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.BACKTICK:
|
||||
// Ideally this would be CTRL+P for "Profile", but that's
|
||||
// taken by the print dialog. CTRL+I for "Information"
|
||||
|
@ -632,6 +649,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
<HostSignupContainer />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -80,6 +80,8 @@ import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityProt
|
|||
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import DialPadModal from "../views/voip/DialPadModal";
|
||||
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -217,6 +219,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
private screenAfterLogin?: IScreen;
|
||||
private windowWidth: number;
|
||||
private pageChanging: boolean;
|
||||
private tokenLogin?: boolean;
|
||||
private accountPassword?: string;
|
||||
private accountPasswordTimer?: NodeJS.Timeout;
|
||||
private focusComposer: boolean;
|
||||
|
@ -322,13 +325,21 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
Lifecycle.attemptTokenLogin(
|
||||
this.props.realQueryParams,
|
||||
this.props.defaultDeviceDisplayName,
|
||||
).then((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
this.getFragmentAfterLogin(),
|
||||
).then(async (loggedIn) => {
|
||||
if (this.props.realQueryParams?.loginToken) {
|
||||
// remove the loginToken from the URL regardless
|
||||
this.props.onTokenLoginCompleted();
|
||||
}
|
||||
|
||||
// don't do anything else until the page reloads - just stay in
|
||||
// the 'loading' state.
|
||||
return;
|
||||
if (loggedIn) {
|
||||
this.tokenLogin = true;
|
||||
|
||||
// Create and start the client
|
||||
await Lifecycle.restoreFromLocalStorage({
|
||||
ignoreGuest: true,
|
||||
});
|
||||
return this.postLoginSetup();
|
||||
}
|
||||
|
||||
// if the user has followed a login or register link, don't reanimate
|
||||
|
@ -352,6 +363,42 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||
}
|
||||
|
||||
private async postLoginSetup() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
if (!cryptoEnabled) {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
promisesList.push(cli.downloadKeys([cli.getUserId()]));
|
||||
}
|
||||
|
||||
// Now update the state to say we're waiting for the first sync to complete rather
|
||||
// than for the login to finish.
|
||||
this.setState({ pendingInitialSync: true });
|
||||
|
||||
await Promise.all(promisesList);
|
||||
|
||||
if (!cryptoEnabled) {
|
||||
this.setState({ pendingInitialSync: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
if (crossSigningIsSetUp) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
this.setState({ pendingInitialSync: false });
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillUpdate(props, state) {
|
||||
|
@ -703,8 +750,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.state.resizeNotifier.notifyLeftHandleResized();
|
||||
});
|
||||
break;
|
||||
case Action.OpenDialPad:
|
||||
Modal.createTrackedDialog('Dial pad', '', DialPadModal, {}, "mx_Dialog_dialPadWrapper");
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
if (
|
||||
// Skip this handling for token login as that always calls onLoggedIn itself
|
||||
!this.tokenLogin &&
|
||||
!Lifecycle.isSoftLogout() &&
|
||||
this.state.view !== Views.LOGIN &&
|
||||
this.state.view !== Views.REGISTER &&
|
||||
|
@ -1182,6 +1234,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
) {
|
||||
showAnalyticsToast(this.props.config.piwik?.policyUrl);
|
||||
}
|
||||
if (SdkConfig.get().mobileGuideToast) {
|
||||
// The toast contains further logic to detect mobile platforms,
|
||||
// check if it has been dismissed before, etc.
|
||||
showMobileGuideToast();
|
||||
}
|
||||
}
|
||||
|
||||
private showScreenAfterLogin() {
|
||||
|
@ -1318,6 +1375,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
cli.on('Session.logged_out', function(errObj) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
|
||||
// A modal might have been open when we were logged out by the server
|
||||
Modal.closeCurrentModal('Session.logged_out');
|
||||
|
||||
if (errObj.httpStatus === 401 && errObj.data && errObj.data['soft_logout']) {
|
||||
console.warn("Soft logout issued by server - avoiding data deletion");
|
||||
Lifecycle.softLogout();
|
||||
|
@ -1328,6 +1388,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
title: _t('Signed Out'),
|
||||
description: _t('For security, this session has been signed out. Please sign in again.'),
|
||||
});
|
||||
|
||||
dis.dispatch({
|
||||
action: 'logout',
|
||||
});
|
||||
|
@ -1597,10 +1658,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
|
||||
|
||||
let threepidInvite: IThreepidInvite;
|
||||
// if we landed here from a 3PID invite, persist it
|
||||
if (params.signurl && params.email) {
|
||||
threepidInvite = ThreepidInviteStore.instance
|
||||
.storeInvite(roomString, params as IThreepidInviteWireFormat);
|
||||
}
|
||||
// otherwise check that this room doesn't already have a known invite
|
||||
if (!threepidInvite) {
|
||||
const invites = ThreepidInviteStore.instance.getInvites();
|
||||
threepidInvite = invites.find(invite => invite.roomId === roomString);
|
||||
}
|
||||
|
||||
// on our URLs there might be a ?via=matrix.org or similar to help
|
||||
// joins to the room succeed. We'll pass these through as an array
|
||||
|
@ -1829,40 +1896,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// Create and start the client
|
||||
await Lifecycle.setLoggedIn(credentials);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cryptoEnabled = cli.isCryptoEnabled();
|
||||
if (!cryptoEnabled) {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
promisesList.push(cli.downloadKeys([cli.getUserId()]));
|
||||
}
|
||||
|
||||
// Now update the state to say we're waiting for the first sync to complete rather
|
||||
// than for the login to finish.
|
||||
this.setState({ pendingInitialSync: true });
|
||||
|
||||
await Promise.all(promisesList);
|
||||
|
||||
if (!cryptoEnabled) {
|
||||
this.setState({ pendingInitialSync: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
|
||||
if (crossSigningIsSetUp) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
}
|
||||
this.setState({ pendingInitialSync: false });
|
||||
await this.postLoginSetup();
|
||||
};
|
||||
|
||||
// complete security / e2e setup has finished
|
||||
|
@ -1906,6 +1940,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
<E2eSetup
|
||||
onFinished={this.onCompleteSecurityE2eSetupFinished}
|
||||
accountPassword={this.accountPassword}
|
||||
tokenLogin={!!this.tokenLogin}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.view === Views.LOGGED_IN) {
|
||||
|
|
|
@ -23,9 +23,11 @@ import classNames from 'classnames';
|
|||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import * as sdk from '../../index';
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {Layout, LayoutPropType} from "../../settings/Layout";
|
||||
import {_t} from "../../languageHandler";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
|
@ -135,14 +137,13 @@ export default class MessagePanel extends React.Component {
|
|||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
|
||||
// whether or not to show flair at all
|
||||
enableFlair: PropTypes.bool,
|
||||
};
|
||||
|
||||
// Force props to be loaded for useIRCLayout
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -207,11 +208,13 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -224,6 +227,14 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case "scroll_to_bottom":
|
||||
this.scrollToBottom();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onShowTypingNotificationsChange = () => {
|
||||
this.setState({
|
||||
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||
|
@ -612,7 +623,7 @@ export default class MessagePanel extends React.Component {
|
|||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
layout={this.props.layout}
|
||||
enableFlair={this.props.enableFlair}
|
||||
/>
|
||||
</TileErrorBoundary>
|
||||
|
@ -810,7 +821,7 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
let ircResizer = null;
|
||||
if (this.props.useIRCLayout) {
|
||||
if (this.props.layout == Layout.IRC) {
|
||||
ircResizer = <IRCTimelineProfileResizer
|
||||
minWidth={20}
|
||||
maxWidth={600}
|
||||
|
|
|
@ -39,7 +39,7 @@ class NotificationPanel extends React.Component {
|
|||
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{_t('You’re all caught up')}</h2>
|
||||
<p>{_t('You have no visible notifications in this room.')}</p>
|
||||
<p>{_t('You have no visible notifications.')}</p>
|
||||
</div>);
|
||||
|
||||
let content;
|
||||
|
|
|
@ -30,7 +30,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
|||
import {Action} from "../../dispatcher/actions";
|
||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
|
@ -186,7 +185,7 @@ export default class RightPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onCloseUserInfo = () => {
|
||||
onClose = () => {
|
||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||
// things are in... this knows far more than it should do about the state of the rest
|
||||
// of the app and is generally a bit silly.
|
||||
|
@ -198,31 +197,21 @@ export default class RightPanel extends React.Component {
|
|||
dis.dispatch({
|
||||
action: "view_home_page",
|
||||
});
|
||||
} else if (this.state.phase === RightPanelPhases.EncryptionPanel &&
|
||||
} else if (
|
||||
this.state.phase === RightPanelPhases.EncryptionPanel &&
|
||||
this.state.verificationRequest && this.state.verificationRequest.pending
|
||||
) {
|
||||
// When the user clicks close on the encryption panel cancel the pending request first if any
|
||||
this.state.verificationRequest.cancel();
|
||||
} else {
|
||||
// Otherwise we have got our user from RoomViewStore which means we're being shown
|
||||
// within a room/group, so go back to the member panel if we were in the encryption panel,
|
||||
// or the member list if we were in the member panel... phew.
|
||||
const isEncryptionPhase = this.state.phase === RightPanelPhases.EncryptionPanel;
|
||||
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: isEncryptionPhase ? this.state.member : null,
|
||||
action: Action.ToggleRightPanel,
|
||||
type: this.props.groupId ? "group" : "room",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ToggleRightPanel,
|
||||
type: this.props.groupId ? "group" : "room",
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
|
@ -260,7 +249,7 @@ export default class RightPanel extends React.Component {
|
|||
user={this.state.member}
|
||||
room={this.props.room}
|
||||
key={roomId || this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo}
|
||||
onClose={this.onClose}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
|
@ -276,7 +265,7 @@ export default class RightPanel extends React.Component {
|
|||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo} />;
|
||||
onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.GroupRoomInfo:
|
||||
|
|
|
@ -477,7 +477,7 @@ export default class RoomDirectory extends React.Component {
|
|||
dis.dispatch(payload);
|
||||
}
|
||||
|
||||
getRow(room) {
|
||||
createRoomCells(room) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const clientRoom = client.getRoom(room.room_id);
|
||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||
|
@ -487,7 +487,11 @@ export default class RoomDirectory extends React.Component {
|
|||
let previewButton;
|
||||
let joinOrViewButton;
|
||||
|
||||
if (room.world_readable && !hasJoinedRoom) {
|
||||
// Element Web currently does not allow guests to join rooms, so we
|
||||
// instead show them preview buttons for all rooms. If the room is not
|
||||
// world readable, a modal will appear asking you to register first. If
|
||||
// it is readable, the preview appears as normal.
|
||||
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
||||
previewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>{_t("Preview")}</AccessibleButton>
|
||||
);
|
||||
|
@ -496,7 +500,7 @@ export default class RoomDirectory extends React.Component {
|
|||
joinOrViewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>{_t("View")}</AccessibleButton>
|
||||
);
|
||||
} else if (!isGuest || room.guest_can_join) {
|
||||
} else if (!isGuest) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>{_t("Join")}</AccessibleButton>
|
||||
);
|
||||
|
@ -519,31 +523,56 @@ export default class RoomDirectory extends React.Component {
|
|||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
room.avatar_url, 32, 32, "crop",
|
||||
);
|
||||
return (
|
||||
<tr key={ room.room_id }
|
||||
return [
|
||||
<div key={ `${room.room_id}_avatar` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ avatarUrl } />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }} />
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomMemberCount">
|
||||
{ room.num_joined_members }
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_preview">{previewButton}</td>
|
||||
<td className="mx_RoomDirectory_join">{joinOrViewButton}</td>
|
||||
</tr>
|
||||
);
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ avatarUrl }
|
||||
/>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_description` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomDescription"
|
||||
>
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }}
|
||||
/>
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_memberCount` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomMemberCount"
|
||||
>
|
||||
{ room.num_joined_members }
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_preview` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_preview"
|
||||
>
|
||||
{previewButton}
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_join` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_join"
|
||||
>
|
||||
{joinOrViewButton}
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
||||
collectScrollPanel = (element) => {
|
||||
|
@ -602,7 +631,8 @@ export default class RoomDirectory extends React.Component {
|
|||
} else if (this.state.protocolsLoading) {
|
||||
content = <Loader />;
|
||||
} else {
|
||||
const rows = (this.state.publicRooms || []).map(room => this.getRow(room));
|
||||
const cells = (this.state.publicRooms || [])
|
||||
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],);
|
||||
// we still show the scrollpanel, at least for now, because
|
||||
// otherwise we don't fetch more because we don't get a fill
|
||||
// request from the scrollpanel because there isn't one
|
||||
|
@ -613,14 +643,12 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
|
||||
let scrollpanel_content;
|
||||
if (rows.length === 0 && !this.state.loading) {
|
||||
if (cells.length === 0 && !this.state.loading) {
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
scrollpanel_content = <table className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>;
|
||||
scrollpanel_content = <div className="mx_RoomDirectory_table">
|
||||
{ cells }
|
||||
</div>;
|
||||
}
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = <ScrollPanel ref={this.collectScrollPanel}
|
||||
|
|
|
@ -21,15 +21,15 @@ limitations under the License.
|
|||
// - Search results component
|
||||
// - Drag and drop
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {_t} from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
|
@ -40,23 +40,24 @@ import Tinter from '../../Tinter';
|
|||
import rateLimitedFunc from '../../ratelimitedfunc';
|
||||
import * as ObjectUtils from '../../ObjectUtils';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, {searchPagination} from '../../Searching';
|
||||
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard';
|
||||
import eventSearch, { searchPagination } from '../../Searching';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key } from '../../Keyboard';
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {Layout} from "../../settings/Layout";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import {SettingLevel} from "../../settings/SettingLevel";
|
||||
import {IMatrixClientCreds} from "../../MatrixClientPeg";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
|
@ -67,17 +68,18 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
|||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import {XOR} from "../../@types/common";
|
||||
import { XOR } from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import {containsEmoji} from '../../effects/utils';
|
||||
import {CHAT_EFFECTS} from '../../effects';
|
||||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -180,7 +182,7 @@ export interface IState {
|
|||
};
|
||||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
useIRCLayout: boolean;
|
||||
layout: Layout;
|
||||
matrixClientIsReady: boolean;
|
||||
showUrlPreview?: boolean;
|
||||
e2eStatus?: E2EStatus;
|
||||
|
@ -236,7 +238,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
statusBarVisible: false,
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
};
|
||||
|
@ -265,13 +267,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
||||
this.onReadReceiptsChange);
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Move into constructor
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
|
||||
}
|
||||
|
||||
private onWidgetStoreUpdate = () => {
|
||||
|
@ -282,8 +278,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
private checkWidgets = (room) => {
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetStore.instance.getPinnedApps(room.roomId).length > 0,
|
||||
})
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top).length > 0,
|
||||
showApps: this.shouldShowApps(room),
|
||||
});
|
||||
};
|
||||
|
||||
private onReadReceiptsChange = () => {
|
||||
|
@ -420,11 +417,17 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
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)
|
||||
|
@ -490,7 +493,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private shouldShowApps(room: Room) {
|
||||
if (!BROWSER_SUPPORTS_SANDBOX) return false;
|
||||
if (!BROWSER_SUPPORTS_SANDBOX || !room) return false;
|
||||
|
||||
// Check if user has previously chosen to hide the app drawer for this
|
||||
// room. If so, do not show apps
|
||||
|
@ -499,10 +502,15 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
// This is confusing, but it means to say that we default to the tray being
|
||||
// hidden unless the user clicked to open it.
|
||||
return hideWidgetDrawer === "false";
|
||||
const isManuallyShown = hideWidgetDrawer === "false";
|
||||
|
||||
const widgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
|
||||
return widgets.length > 0 || isManuallyShown;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
|
||||
const call = this.getCallForRoom();
|
||||
const callState = call ? call.state : null;
|
||||
this.setState({
|
||||
|
@ -610,6 +618,13 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
if (this.state.room) {
|
||||
WidgetLayoutStore.instance.off(
|
||||
WidgetLayoutStore.emissionForRoom(this.state.room),
|
||||
this.onWidgetLayoutChange,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.showReadReceiptsWatchRef) {
|
||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
||||
}
|
||||
|
@ -626,7 +641,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
private onLayoutChange = () => {
|
||||
this.setState({
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -750,6 +765,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'focus_search':
|
||||
this.onSearchClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -837,6 +855,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// called when state.room is first initialised (either at initial load,
|
||||
// after a successful peek, or after we join the room).
|
||||
private onRoomLoaded = (room: Room) => {
|
||||
// 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);
|
||||
this.loadMembersIfJoined(room);
|
||||
|
@ -899,6 +921,15 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
if (!room || room.roomId !== this.state.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detach the listener if the room is changing for some reason
|
||||
if (this.state.room) {
|
||||
WidgetLayoutStore.instance.off(
|
||||
WidgetLayoutStore.emissionForRoom(this.state.room),
|
||||
this.onWidgetLayoutChange,
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
room: room,
|
||||
}, () => {
|
||||
|
@ -1939,8 +1970,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
const messagePanelClassNames = classNames(
|
||||
"mx_RoomView_messagePanel",
|
||||
{
|
||||
"mx_IRCLayout": this.state.useIRCLayout,
|
||||
"mx_GroupLayout": !this.state.useIRCLayout,
|
||||
"mx_IRCLayout": this.state.layout == Layout.IRC,
|
||||
"mx_GroupLayout": this.state.layout == Layout.Group,
|
||||
});
|
||||
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
|
@ -1963,7 +1994,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
layout={this.state.layout}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
|
|
@ -20,7 +20,6 @@ import * as React from "react";
|
|||
import {_t} from '../../languageHandler';
|
||||
import * as sdk from "../../index";
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {LayoutPropType} from "../../settings/Layout";
|
||||
import React, {createRef} from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -111,8 +112,8 @@ class TimelinePanel extends React.Component {
|
|||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
}
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
|
@ -1442,7 +1443,7 @@ class TimelinePanel extends React.Component {
|
|||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editState={this.state.editState}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -28,7 +28,6 @@ import Modal from "../../Modal";
|
|||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
|
@ -51,6 +50,8 @@ 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";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -272,7 +273,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let topSection;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup;
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||
|
@ -292,24 +293,19 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
})}
|
||||
</div>
|
||||
)
|
||||
} else if (signupLink) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => (
|
||||
<a
|
||||
href={signupLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
tabIndex={-1}
|
||||
>{sub}</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (hostSignupConfig) {
|
||||
if (hostSignupConfig && hostSignupConfig.url) {
|
||||
// If hostSignup.domains is set to a non-empty array, only show
|
||||
// dialog if the user is on the domain or a subdomain.
|
||||
const hostSignupDomains = hostSignupConfig.domains || [];
|
||||
const mxDomain = MatrixClientPeg.get().getDomain();
|
||||
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
||||
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
||||
topSection = <div onClick={this.onCloseMenu}>
|
||||
<HostSignupAction />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let homeButton = null;
|
||||
|
|
|
@ -24,6 +24,7 @@ export default class E2eSetup extends React.Component {
|
|||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -33,6 +34,7 @@ export default class E2eSetup extends React.Component {
|
|||
<CreateCrossSigningDialog
|
||||
onFinished={this.props.onFinished}
|
||||
accountPassword={this.props.accountPassword}
|
||||
tokenLogin={this.props.tokenLogin}
|
||||
/>
|
||||
</CompleteSecurityBody>
|
||||
</AuthPage>
|
||||
|
|
|
@ -340,8 +340,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
};
|
||||
|
||||
onTryRegisterClick = ev => {
|
||||
const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
|
||||
const ssoFlow = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
|
||||
const hasPasswordFlow = this.state.flows?.find(flow => flow.type === "m.login.password");
|
||||
const ssoFlow = this.state.flows?.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
|
||||
// If has no password flow but an SSO flow guess that the user wants to register with SSO.
|
||||
// TODO: instead hide the Register button if registration is disabled by checking with the server,
|
||||
// has no specific errCode currently and uses M_FORBIDDEN.
|
||||
|
|
|
@ -120,9 +120,9 @@ export default class SetupEncryptionBody extends React.Component {
|
|||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
|
||||
recoveryKeyPrompt = _t("Use Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key");
|
||||
recoveryKeyPrompt = _t("Use Security Key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
|
|
|
@ -72,9 +72,12 @@ export default class SoftLogout extends React.Component {
|
|||
|
||||
this._initLogin();
|
||||
|
||||
MatrixClientPeg.get().countSessionsNeedingBackup().then(remaining => {
|
||||
this.setState({keyBackupNeeded: remaining > 0});
|
||||
});
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.isCryptoEnabled()) {
|
||||
cli.countSessionsNeedingBackup().then(remaining => {
|
||||
this.setState({ keyBackupNeeded: remaining > 0 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClearAll = () => {
|
||||
|
|
|
@ -609,8 +609,12 @@ export class SSOAuthEntry extends React.Component {
|
|||
this.props.authSessionId,
|
||||
);
|
||||
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
|
||||
this.state = {
|
||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||
attemptFailed: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -618,12 +622,35 @@ export class SSOAuthEntry extends React.Component {
|
|||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("message", this._onReceiveMessage);
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
attemptFailed = () => {
|
||||
this.setState({
|
||||
attemptFailed: true,
|
||||
});
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
||||
if (this._popupWindow) {
|
||||
this._popupWindow.close();
|
||||
this._popupWindow = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onStartAuthClick = () => {
|
||||
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
||||
// certainly will need to open the thing in a new tab to avoid losing application
|
||||
// context.
|
||||
|
||||
window.open(this._ssoUrl, '_blank');
|
||||
this._popupWindow = window.open(this._ssoUrl, "_blank");
|
||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||
};
|
||||
|
@ -656,10 +683,28 @@ export class SSOAuthEntry extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>;
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.attemptFailed) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ _t("Something went wrong in confirming your identity. Cancel and try again.") }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
{ errorSection }
|
||||
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -710,8 +755,7 @@ export class FallbackAuthEntry extends React.Component {
|
|||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this._popupWindow.opener = null;
|
||||
this._popupWindow = window.open(url, "_blank");
|
||||
};
|
||||
|
||||
_onReceiveMessage = event => {
|
||||
|
|
|
@ -196,7 +196,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
|
|
|
@ -194,7 +194,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
|
|||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
|
|
|
@ -20,6 +20,8 @@ import { _t } from '../../../languageHandler';
|
|||
import { ContextMenu, IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
call: MatrixCall;
|
||||
|
@ -46,14 +48,30 @@ export default class CallContextMenu extends React.Component<IProps> {
|
|||
this.props.onFinished();
|
||||
}
|
||||
|
||||
onTransferClick = () => {
|
||||
Modal.createTrackedDialog(
|
||||
'Transfer Call', '', InviteDialog, {kind: KIND_CALL_TRANSFER, call: this.props.call},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
render() {
|
||||
const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold");
|
||||
const handler = this.props.call.isRemoteOnHold() ? this.onUnholdClick : this.onHoldClick;
|
||||
|
||||
let transferItem;
|
||||
if (this.props.call.opponentCanBeTransferred()) {
|
||||
transferItem = <MenuItem className="mx_CallContextMenu_item" onClick={this.onTransferClick}>
|
||||
{_t("Transfer")}
|
||||
</MenuItem>;
|
||||
}
|
||||
|
||||
return <ContextMenu {...this.props}>
|
||||
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
||||
{holdUnholdCaption}
|
||||
</MenuItem>
|
||||
{transferItem}
|
||||
</ContextMenu>;
|
||||
}
|
||||
}
|
||||
|
|
59
src/components/views/context_menus/DialpadContextMenu.tsx
Normal file
59
src/components/views/context_menus/DialpadContextMenu.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import Dialpad from '../voip/DialPad';
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
call: MatrixCall;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class DialpadContextMenu extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: '',
|
||||
}
|
||||
}
|
||||
|
||||
onDigitPress = (digit) => {
|
||||
this.props.call.sendDtmfDigit(digit);
|
||||
this.setState({value: this.state.value + digit});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <ContextMenu {...this.props}>
|
||||
<div className="mx_DialPadContextMenu_header">
|
||||
<div>
|
||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div>
|
||||
</div>
|
||||
<div className="mx_DialPadContextMenu_horizSep" />
|
||||
<div className="mx_DialPadContextMenu_dialPad">
|
||||
<Dialpad onDigitPress={this.onDigitPress} hasDialAndDelete={false} />
|
||||
</div>
|
||||
</ContextMenu>;
|
||||
}
|
||||
}
|
|
@ -20,17 +20,17 @@ import {MatrixCapabilities} from "matrix-widget-api";
|
|||
import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu";
|
||||
import {ChevronFace} from "../../structures/ContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||
import {IApp} from "../../../stores/WidgetStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import Modal from "../../../Modal";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||
app: IApp;
|
||||
|
@ -57,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
let unpinButton;
|
||||
if (showUnpin) {
|
||||
const onUnpinClick = () => {
|
||||
WidgetStore.instance.unpinWidget(room.roomId, app.id);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
@ -127,7 +127,8 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
console.info("Revoking permission for widget to load: " + app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
current[app.eventId] = false;
|
||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).catch(err => {
|
||||
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
|
||||
SettingsStore.setValue("allowedWidgets", roomId, level, current).catch(err => {
|
||||
console.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
});
|
||||
|
@ -137,13 +138,13 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />;
|
||||
}
|
||||
|
||||
const pinnedWidgets = WidgetStore.instance.getPinnedApps(roomId);
|
||||
const pinnedWidgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
|
||||
const widgetIndex = pinnedWidgets.findIndex(widget => widget.id === app.id);
|
||||
|
||||
let moveLeftButton;
|
||||
if (showUnpin && widgetIndex > 0) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, -1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
@ -153,7 +154,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
let moveRightButton;
|
||||
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
|
||||
const onClick = () => {
|
||||
WidgetStore.instance.movePinnedWidget(roomId, app.id, 1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ import {
|
|||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
|
@ -701,6 +703,97 @@ class VerificationExplorer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class WidgetExplorer extends React.Component {
|
||||
static getLabel() {
|
||||
return _t("Active Widgets");
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
query: '',
|
||||
editWidget: null, // set to an IApp when editing
|
||||
};
|
||||
}
|
||||
|
||||
onWidgetStoreUpdate = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onQueryChange = (query) => {
|
||||
this.setState({query});
|
||||
};
|
||||
|
||||
onEditWidget = (widget) => {
|
||||
this.setState({editWidget: widget});
|
||||
};
|
||||
|
||||
onBack = () => {
|
||||
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
|
||||
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
|
||||
this.setState({editWidget: null});
|
||||
} else {
|
||||
this.props.onBack();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
WidgetStore.instance.off(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
}
|
||||
|
||||
render() {
|
||||
const room = this.props.room;
|
||||
|
||||
const editWidget = this.state.editWidget;
|
||||
const widgets = WidgetStore.instance.getApps(room.roomId);
|
||||
if (editWidget && widgets.includes(editWidget)) {
|
||||
const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values()))
|
||||
.reduce((p, c) => {p.push(...c); return p;}, []);
|
||||
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
||||
if (!stateEv) { // "should never happen"
|
||||
return <div>
|
||||
{_t("There was an error finding this widget.")}
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
return <SendCustomEvent
|
||||
onBack={this.onBack}
|
||||
room={room}
|
||||
forceStateEvent={true}
|
||||
inputs={{
|
||||
eventType: stateEv.getType(),
|
||||
evContent: JSON.stringify(stateEv.getContent(), null, '\t'),
|
||||
stateKey: stateEv.getStateKey(),
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
<FilteredList query={this.state.query} onChange={this.onQueryChange}>
|
||||
{widgets.map(w => {
|
||||
return <button
|
||||
className='mx_DevTools_RoomStateExplorer_button'
|
||||
key={w.url + w.eventId}
|
||||
onClick={() => this.onEditWidget(w)}
|
||||
>{w.url}</button>;
|
||||
})}
|
||||
</FilteredList>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const Entries = [
|
||||
SendCustomEvent,
|
||||
RoomStateExplorer,
|
||||
|
@ -708,6 +801,7 @@ const Entries = [
|
|||
AccountDataExplorer,
|
||||
ServersInRoomList,
|
||||
VerificationExplorer,
|
||||
WidgetExplorer,
|
||||
];
|
||||
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
|
|
|
@ -50,6 +50,10 @@ export default class ErrorDialog extends React.Component {
|
|||
button: null,
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
|
@ -64,7 +68,7 @@ export default class ErrorDialog extends React.Component {
|
|||
{ this.props.description || _t('An error has occurred.') }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||
<button className="mx_Dialog_primary" onClick={this.onClick} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
</div>
|
||||
|
|
291
src/components/views/dialogs/HostSignupDialog.tsx
Normal file
291
src/components/views/dialogs/HostSignupDialog.tsx
Normal file
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import PersistedElement from "../elements/PersistedElement";
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import classNames from "classnames";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import {
|
||||
IHostSignupConfig,
|
||||
IPostmessage,
|
||||
IPostmessageResponseData,
|
||||
PostmessageAction,
|
||||
} from "./HostSignupDialogTypes";
|
||||
|
||||
const HOST_SIGNUP_KEY = "host_signup";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {
|
||||
completed: boolean;
|
||||
error: string;
|
||||
minimized: boolean;
|
||||
}
|
||||
|
||||
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
|
||||
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
private readonly config: IHostSignupConfig;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
completed: false,
|
||||
error: null,
|
||||
minimized: false,
|
||||
};
|
||||
|
||||
this.config = SdkConfig.get().hostSignup;
|
||||
}
|
||||
|
||||
private messageHandler = async (message: IPostmessage) => {
|
||||
if (!this.config.url.startsWith(message.origin)) {
|
||||
return;
|
||||
}
|
||||
switch (message.data.action) {
|
||||
case PostmessageAction.HostSignupAccountDetailsRequest:
|
||||
this.onAccountDetailsRequest();
|
||||
break;
|
||||
case PostmessageAction.Maximize:
|
||||
this.setState({
|
||||
minimized: false,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.Minimize:
|
||||
this.setState({
|
||||
minimized: true,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.SetupComplete:
|
||||
this.setState({
|
||||
completed: true,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.CloseDialog:
|
||||
return this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private maximizeDialog = () => {
|
||||
this.setState({
|
||||
minimized: false,
|
||||
});
|
||||
// Send this action to the iframe so it can act accordingly
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.Maximize,
|
||||
});
|
||||
}
|
||||
|
||||
private minimizeDialog = () => {
|
||||
this.setState({
|
||||
minimized: true,
|
||||
});
|
||||
// Send this action to the iframe so it can act accordingly
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.Minimize,
|
||||
});
|
||||
}
|
||||
|
||||
private closeDialog = async () => {
|
||||
window.removeEventListener("message", this.messageHandler);
|
||||
// Ensure we destroy the host signup persisted element
|
||||
PersistedElement.destroyElement("host_signup");
|
||||
// Finally clear the flag in
|
||||
return HostSignupStore.instance.setHostSignupActive(false);
|
||||
}
|
||||
|
||||
private onCloseClick = async () => {
|
||||
if (this.state.completed) {
|
||||
// We're done, close
|
||||
return this.closeDialog();
|
||||
} else {
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("Confirm abort of host creation"),
|
||||
description: _t(
|
||||
"Are you sure you wish to abort creation of the host? The process cannot be continued.",
|
||||
),
|
||||
button: _t("Abort"),
|
||||
onFinished: result => {
|
||||
if (result) {
|
||||
return this.closeDialog();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage = (message: IPostmessageResponseData) => {
|
||||
this.iframeRef.current.contentWindow.postMessage(message, this.config.url);
|
||||
}
|
||||
|
||||
private async sendAccountDetails() {
|
||||
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
if (!openIdToken || !openIdToken.access_token) {
|
||||
console.warn("Failed to connect to homeserver for OpenID token.")
|
||||
this.setState({
|
||||
completed: true,
|
||||
error: _t("Failed to connect to your homeserver. Please close this dialog and try again."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.HostSignupAccountDetails,
|
||||
account: {
|
||||
accessToken: await MatrixClientPeg.get().getAccessToken(),
|
||||
name: OwnProfileStore.instance.displayName,
|
||||
openIdToken: openIdToken.access_token,
|
||||
serverName: await MatrixClientPeg.get().getDomain(),
|
||||
userLocalpart: await MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
termsAccepted: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private onAccountDetailsDialogFinished = async (result) => {
|
||||
if (result) {
|
||||
return this.sendAccountDetails();
|
||||
}
|
||||
return this.closeDialog();
|
||||
}
|
||||
|
||||
private onAccountDetailsRequest = () => {
|
||||
const textComponent = (
|
||||
<>
|
||||
<p>
|
||||
{_t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
|
||||
"account to fetch verified email addresses. This data is not stored.", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{_t("Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
|
||||
{},
|
||||
{
|
||||
cookiePolicyLink: () => (
|
||||
<a href={this.config.cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Cookie Policy")}
|
||||
</a>
|
||||
),
|
||||
privacyPolicyLink: () => (
|
||||
<a href={this.config.privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Privacy Policy")}
|
||||
</a>
|
||||
),
|
||||
termsOfServiceLink: () => (
|
||||
<a href={this.config.termsOfServiceUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Terms of Service")}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("You should know"),
|
||||
description: textComponent,
|
||||
button: _t("Continue"),
|
||||
onFinished: this.onAccountDetailsDialogFinished,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener("message", this.messageHandler);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (HostSignupStore.instance.isHostSignupActive) {
|
||||
// Run the close dialog actions if we're still active, otherwise good to go
|
||||
return this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_HostSignup_persisted">
|
||||
<PersistedElement key={HOST_SIGNUP_KEY} persistKey={HOST_SIGNUP_KEY}>
|
||||
<div className={classNames({ "mx_Dialog_wrapper": !this.state.minimized })}>
|
||||
<div
|
||||
className={classNames("mx_Dialog",
|
||||
{
|
||||
"mx_HostSignupDialog_minimized": this.state.minimized,
|
||||
"mx_HostSignupDialog": !this.state.minimized,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithButton">
|
||||
<div className="mx_Dialog_title">
|
||||
{_t("%(hostSignupBrand)s Setup", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className="mx_HostSignup_maximize_button"
|
||||
onClick={this.maximizeDialog}
|
||||
aria-label={_t("Maximize dialog")}
|
||||
title={_t("Maximize dialog")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithCancel">
|
||||
<AccessibleButton
|
||||
onClick={this.minimizeDialog}
|
||||
className="mx_HostSignup_minimize_button"
|
||||
aria-label={_t("Minimize dialog")}
|
||||
title={_t("Minimize dialog")}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onCloseClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
aria-label={_t("Close dialog")}
|
||||
title={_t("Close dialog")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.state.error &&
|
||||
<div>
|
||||
{this.state.error}
|
||||
</div>
|
||||
}
|
||||
{!this.state.error &&
|
||||
<iframe
|
||||
src={this.config.url}
|
||||
ref={this.iframeRef}
|
||||
sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PersistedElement>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
56
src/components/views/dialogs/HostSignupDialogTypes.ts
Normal file
56
src/components/views/dialogs/HostSignupDialogTypes.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum PostmessageAction {
|
||||
CloseDialog = "close_dialog",
|
||||
HostSignupAccountDetails = "host_signup_account_details",
|
||||
HostSignupAccountDetailsRequest = "host_signup_account_details_request",
|
||||
Minimize = "host_signup_minimize",
|
||||
Maximize = "host_signup_maximize",
|
||||
SetupComplete = "setup_complete",
|
||||
}
|
||||
|
||||
interface IAccountData {
|
||||
accessToken: string;
|
||||
name: string;
|
||||
openIdToken: string;
|
||||
serverName: string;
|
||||
userLocalpart: string;
|
||||
termsAccepted: boolean;
|
||||
}
|
||||
|
||||
export interface IPostmessageRequestData {
|
||||
action: PostmessageAction;
|
||||
}
|
||||
|
||||
export interface IPostmessageResponseData {
|
||||
action: PostmessageAction;
|
||||
account?: IAccountData;
|
||||
}
|
||||
|
||||
export interface IPostmessage {
|
||||
data: IPostmessageRequestData;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
export interface IHostSignupConfig {
|
||||
brand: string;
|
||||
cookiePolicyUrl: string;
|
||||
domains: Array<string>;
|
||||
privacyPolicyUrl: string;
|
||||
termsOfServiceUrl: string;
|
||||
url: string;
|
||||
}
|
|
@ -41,12 +41,14 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
|
@ -310,6 +312,9 @@ interface IInviteDialogProps {
|
|||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: string,
|
||||
|
||||
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||
call: MatrixCall,
|
||||
|
||||
// Initial value to populate the filter with
|
||||
initialText: string,
|
||||
}
|
||||
|
@ -345,6 +350,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|
||||
if (props.kind === KIND_INVITE && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
|
||||
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
|
||||
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
||||
}
|
||||
|
||||
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
|
||||
|
@ -702,6 +709,29 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
});
|
||||
};
|
||||
|
||||
_transferCall = async () => {
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
if (targetIds.length > 1) {
|
||||
this.setState({
|
||||
errorText: _t("A call can only be transferred to a single user."),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({busy: true});
|
||||
try {
|
||||
await this.props.call.transfer(targetIds[0]);
|
||||
this.setState({busy: false});
|
||||
this.props.onFinished();
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to transfer call"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyDown = (e) => {
|
||||
if (this.state.busy) return;
|
||||
const value = e.target.value.trim();
|
||||
|
@ -1217,7 +1247,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else { // KIND_INVITE
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
title = _t("Invite to this room");
|
||||
|
||||
if (identityServersEnabled) {
|
||||
|
@ -1251,6 +1281,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
goButtonFn = this._transferCall;
|
||||
} else {
|
||||
console.error("Unknown kind of InviteDialog: " + this.props.kind);
|
||||
}
|
||||
|
||||
const hasSelection = this.state.targets.length > 0
|
||||
|
|
|
@ -35,13 +35,13 @@ import {
|
|||
} from "matrix-widget-api";
|
||||
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
widgetRoomId?: string;
|
||||
sourceWidgetId: string;
|
||||
onFinished(success: boolean, data?: IModalWidgetReturnData): void;
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
|
||||
public render() {
|
||||
const templated = this.widget.getCompleteUrl({
|
||||
currentRoomId: RoomViewStore.getRoomId(),
|
||||
widgetRoomId: this.props.widgetRoomId,
|
||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||
userDisplayName: OwnProfileStore.instance.displayName,
|
||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
|||
<li>{_t("The internet connection either session is using")}</li>
|
||||
</ul>
|
||||
<div>
|
||||
{_t("We recommend you change your password and recovery key in Settings immediately")}
|
||||
{_t("We recommend you change your password and Security Key in Settings immediately")}
|
||||
</div>
|
||||
</div>,
|
||||
onFinished: () => this.props.onFinished(false),
|
||||
|
|
|
@ -44,7 +44,8 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
|
|||
const [email, setEmail] = useState("");
|
||||
const fieldRef = useRef<Field>();
|
||||
|
||||
const onSubmit = async () => {
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
const valid = await fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
|
@ -73,6 +74,7 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
|
|||
<form onSubmit={onSubmit}>
|
||||
<Field
|
||||
ref={fieldRef}
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
label={_t("Email (optional)")}
|
||||
value={email}
|
||||
|
|
|
@ -151,13 +151,13 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
|
||||
const valid = await this.fieldRef.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!valid) {
|
||||
if (!valid && !this.state.defaultChosen) {
|
||||
this.fieldRef.current.focus();
|
||||
this.fieldRef.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onFinished(this.validatedConf);
|
||||
this.props.onFinished(this.state.defaultChosen ? this.defaultServer : this.validatedConf);
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -18,6 +18,7 @@ import React, {createRef} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import Field from "../elements/Field";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
export default class TextInputDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -29,6 +30,7 @@ export default class TextInputDialog extends React.Component {
|
|||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
busyMessage: PropTypes.string, // pass _td string
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
hasCancel: PropTypes.bool,
|
||||
|
@ -40,6 +42,7 @@ export default class TextInputDialog extends React.Component {
|
|||
title: "",
|
||||
value: "",
|
||||
description: "",
|
||||
busyMessage: _td("Loading..."),
|
||||
focus: true,
|
||||
hasCancel: true,
|
||||
};
|
||||
|
@ -51,6 +54,7 @@ export default class TextInputDialog extends React.Component {
|
|||
|
||||
this.state = {
|
||||
value: this.props.value,
|
||||
busy: false,
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
@ -66,11 +70,13 @@ export default class TextInputDialog extends React.Component {
|
|||
onOk = async ev => {
|
||||
ev.preventDefault();
|
||||
if (this.props.validator) {
|
||||
this.setState({ busy: true });
|
||||
await this._field.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!this._field.current.state.valid) {
|
||||
this._field.current.focus();
|
||||
this._field.current.validate({ allowEmpty: false, focused: true });
|
||||
this.setState({ busy: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +131,8 @@ export default class TextInputDialog extends React.Component {
|
|||
</div>
|
||||
</form>
|
||||
<DialogButtons
|
||||
primaryButton={this.props.button}
|
||||
primaryButton={this.state.busy ? _t(this.props.busyMessage) : this.props.button}
|
||||
disabled={this.state.busy}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel}
|
||||
hasCancel={this.props.hasCancel}
|
||||
|
|
|
@ -70,26 +70,26 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
|||
return (
|
||||
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("A widget would like to verify your identity")}>
|
||||
title={_t("Allow this widget to verify your identity")}>
|
||||
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"A widget located at %(widgetUrl)s would like to verify your identity. " +
|
||||
"By allowing this, the widget will be able to verify your user ID, but not " +
|
||||
"perform actions as you.", {
|
||||
widgetUrl: this.props.widget.templateUrl.split("?")[0],
|
||||
},
|
||||
)}
|
||||
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
|
||||
</p>
|
||||
<p className="text-muted">
|
||||
{/* cheap trim to just get the path */}
|
||||
{this.props.widget.templateUrl.split("?")[0].split("#")[0]}
|
||||
</p>
|
||||
<LabelledToggleSwitch value={this.state.rememberSelection} toggleInFront={true}
|
||||
onChange={this._onRememberSelectionChange}
|
||||
label={_t("Remember my selection for this widget")} />
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Allow")}
|
||||
primaryButton={_t("Continue")}
|
||||
onPrimaryButtonClick={this._onAllow}
|
||||
cancelButton={_t("Deny")}
|
||||
onCancel={this._onDeny}
|
||||
additive={
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.rememberSelection}
|
||||
toggleInFront={true}
|
||||
onChange={this._onRememberSelectionChange}
|
||||
label={_t("Remember this")} />}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -199,11 +199,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
} else if (this.state.recoveryKeyCorrect) {
|
||||
return _t("Looks good!");
|
||||
} else if (this.state.recoveryKeyValid) {
|
||||
return _t("Wrong Recovery Key");
|
||||
return _t("Wrong Security Key");
|
||||
} else if (this.state.recoveryKeyValid === null) {
|
||||
return '';
|
||||
} else {
|
||||
return _t("Invalid Recovery Key");
|
||||
return _t("Invalid Security Key");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +231,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t(
|
||||
"Unable to access secret storage. " +
|
||||
"Please verify that you entered the correct recovery passphrase.",
|
||||
"Please verify that you entered the correct Security Phrase.",
|
||||
)}
|
||||
</div>;
|
||||
} else {
|
||||
|
@ -298,7 +298,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
|
||||
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
|
||||
<Field
|
||||
type="text"
|
||||
type="password"
|
||||
label={_t('Security Key')}
|
||||
value={this.state.recoveryKey}
|
||||
onChange={this._onRecoveryKeyChange}
|
||||
|
|
|
@ -34,6 +34,7 @@ import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
|||
export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
accountPassword: PropTypes.string,
|
||||
tokenLogin: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -96,6 +97,9 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
user: MatrixClientPeg.get().getUserId(),
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
} else if (this.props.tokenLogin) {
|
||||
// We are hoping the grace period is active
|
||||
await makeRequest({});
|
||||
} else {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
|
@ -144,6 +148,12 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
|||
});
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
if (this.props.tokenLogin) {
|
||||
// ignore any failures, we are relying on grace period here
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping cross-signing", e);
|
||||
}
|
||||
|
|
|
@ -297,19 +297,19 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
} else if (this.state.restoreError) {
|
||||
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
|
||||
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
|
||||
title = _t("Recovery key mismatch");
|
||||
title = _t("Security Key mismatch");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this recovery key: " +
|
||||
"please verify that you entered the correct recovery key.",
|
||||
"Backup could not be decrypted with this Security Key: " +
|
||||
"please verify that you entered the correct Security Key.",
|
||||
)}</p>
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Incorrect recovery passphrase");
|
||||
title = _t("Incorrect Security Phrase");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this recovery passphrase: " +
|
||||
"please verify that you entered the correct recovery passphrase.",
|
||||
"Backup could not be decrypted with this Security Phrase: " +
|
||||
"please verify that you entered the correct Security Phrase.",
|
||||
)}</p>
|
||||
</div>;
|
||||
}
|
||||
|
@ -342,7 +342,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
title = _t("Enter recovery passphrase");
|
||||
title = _t("Enter Security Phrase");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: you should only set up key backup " +
|
||||
|
@ -351,7 +351,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
)}</p>
|
||||
<p>{_t(
|
||||
"Access your secure message history and set up secure " +
|
||||
"messaging by entering your recovery passphrase.",
|
||||
"messaging by entering your Security Phrase.",
|
||||
)}</p>
|
||||
|
||||
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
|
@ -371,8 +371,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
/>
|
||||
</form>
|
||||
{_t(
|
||||
"If you've forgotten your recovery passphrase you can "+
|
||||
"<button1>use your recovery key</button1> or " +
|
||||
"If you've forgotten your Security Phrase you can "+
|
||||
"<button1>use your Security Key</button1> or " +
|
||||
"<button2>set up new recovery options</button2>"
|
||||
, {}, {
|
||||
button1: s => <AccessibleButton className="mx_linkButton"
|
||||
|
@ -390,7 +390,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
})}
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Enter recovery key");
|
||||
title = _t("Enter Security Key");
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
|
@ -399,11 +399,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>;
|
||||
} else if (this.state.recoveryKeyValid) {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
|
||||
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
|
||||
{"\uD83D\uDC4D "}{_t("This looks like a valid Security Key!")}
|
||||
</div>;
|
||||
} else {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
|
||||
{"\uD83D\uDC4E "}{_t("Not a valid Security Key")}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -415,7 +415,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
)}</p>
|
||||
<p>{_t(
|
||||
"Access your secure message history and set up secure " +
|
||||
"messaging by entering your recovery key.",
|
||||
"messaging by entering your Security Key.",
|
||||
)}</p>
|
||||
|
||||
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
|
@ -434,7 +434,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
/>
|
||||
</div>
|
||||
{_t(
|
||||
"If you've forgotten your recovery key you can "+
|
||||
"If you've forgotten your Security Key you can "+
|
||||
"<button>set up new recovery options</button>"
|
||||
, {}, {
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
|
|
|
@ -33,7 +33,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import {aboveLeftOf, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import PersistedElement, {getPersistKey} from "./PersistedElement";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||
import {MatrixCapabilities} from "matrix-widget-api";
|
||||
|
@ -240,7 +239,8 @@ export default class AppTile extends React.Component {
|
|||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||
current[this.props.app.eventId] = true;
|
||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
||||
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
|
||||
SettingsStore.setValue("allowedWidgets", roomId, level, current).then(() => {
|
||||
this.setState({hasPermissionToLoad: true});
|
||||
|
||||
// Fetch a token for the integration manager, now that we're allowed to
|
||||
|
|
173
src/components/views/elements/DesktopCapturerSourcePicker.tsx
Normal file
173
src/components/views/elements/DesktopCapturerSourcePicker.tsx
Normal file
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "..//dialogs/BaseDialog"
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
export interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL;
|
||||
}
|
||||
|
||||
export enum Tabs {
|
||||
Screens = "screens",
|
||||
Windows = "windows",
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourceIProps {
|
||||
source: DesktopCapturerSource;
|
||||
onSelect(source: DesktopCapturerSource): void;
|
||||
}
|
||||
|
||||
export class ExistingSource extends React.Component<DesktopCapturerSourceIProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
this.props.onSelect(this.props.source);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_desktopCapturerSourcePicker_stream_button"
|
||||
title={this.props.source.name}
|
||||
onClick={this.onClick} >
|
||||
<img
|
||||
className="mx_desktopCapturerSourcePicker_stream_thumbnail"
|
||||
src={this.props.source.thumbnailURL}
|
||||
/>
|
||||
<span className="mx_desktopCapturerSourcePicker_stream_name">{this.props.source.name}</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourcePickerIState {
|
||||
selectedTab: Tabs;
|
||||
sources: Array<DesktopCapturerSource>;
|
||||
}
|
||||
export interface DesktopCapturerSourcePickerIProps {
|
||||
onFinished(source: DesktopCapturerSource): void;
|
||||
}
|
||||
|
||||
export default class DesktopCapturerSourcePicker extends React.Component<
|
||||
DesktopCapturerSourcePickerIProps,
|
||||
DesktopCapturerSourcePickerIState
|
||||
> {
|
||||
interval;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedTab: Tabs.Screens,
|
||||
sources: [],
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// setInterval() first waits and then executes, therefore
|
||||
// we call getDesktopCapturerSources() here without any delay.
|
||||
// Otherwise the dialog would be left empty for some time.
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
|
||||
// We update the sources every 500ms to get newer thumbnails
|
||||
this.interval = setInterval(async () => {
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onSelect = (source) => {
|
||||
this.props.onFinished(source);
|
||||
}
|
||||
|
||||
onScreensClick = (ev) => {
|
||||
this.setState({selectedTab: Tabs.Screens});
|
||||
}
|
||||
|
||||
onWindowsClick = (ev) => {
|
||||
this.setState({selectedTab: Tabs.Windows});
|
||||
}
|
||||
|
||||
onCloseClick = (ev) => {
|
||||
this.props.onFinished(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
let sources;
|
||||
if (this.state.selectedTab === Tabs.Screens) {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("screen");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
} else {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("window");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
}
|
||||
|
||||
const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel";
|
||||
const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : "");
|
||||
const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : "");
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_desktopCapturerSourcePicker"
|
||||
onFinished={this.onCloseClick}
|
||||
title={_t("Share your screen")}
|
||||
>
|
||||
<div className="mx_desktopCapturerSourcePicker_tabLabels">
|
||||
<AccessibleButton
|
||||
className={screensButtonStyle}
|
||||
onClick={this.onScreensClick}
|
||||
>
|
||||
{_t("Screens")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={windowsButtonStyle}
|
||||
onClick={this.onWindowsClick}
|
||||
>
|
||||
{_t("Windows")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_desktopCapturerSourcePicker_panel">
|
||||
{ sources }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -124,7 +124,7 @@ export default class EditableItemList extends React.Component {
|
|||
<Field label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
|
|
@ -22,6 +22,7 @@ import * as Avatar from '../../../Avatar';
|
|||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout} from "../../../settings/Layout";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
|
@ -33,7 +34,7 @@ interface IProps {
|
|||
/**
|
||||
* Whether to use the irc layout or not
|
||||
*/
|
||||
useIRCLayout: boolean;
|
||||
layout: Layout;
|
||||
|
||||
/**
|
||||
* classnames to apply to the wrapper of the preview
|
||||
|
@ -121,14 +122,14 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
const event = this.fakeEvent(this.state);
|
||||
|
||||
const className = classnames(this.props.className, {
|
||||
"mx_IRCLayout": this.props.useIRCLayout,
|
||||
"mx_GroupLayout": !this.props.useIRCLayout,
|
||||
"mx_IRCLayout": this.props.layout == Layout.IRC,
|
||||
"mx_GroupLayout": this.props.layout == Layout.Group,
|
||||
});
|
||||
|
||||
return <div className={className}>
|
||||
<EventTile
|
||||
mxEvent={event}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
/>
|
||||
</div>;
|
||||
|
|
|
@ -15,14 +15,15 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {useContext, useRef, useState} from 'react';
|
||||
import {EventType} from 'matrix-js-sdk/src/@types/event';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip from './Tooltip';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useTimeout} from "../../../hooks/useTimeout";
|
||||
import Analytics from "../../../Analytics";
|
||||
import CountlyAnalytics from '../../../CountlyAnalytics';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
|
||||
export const AVATAR_SIZE = 52;
|
||||
|
||||
|
@ -50,6 +51,11 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
|||
|
||||
const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel;
|
||||
|
||||
const {room} = useContext(RoomContext);
|
||||
const canSetAvatar = room?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId());
|
||||
if (!canSetAvatar) return <React.Fragment>{ children }</React.Fragment>;
|
||||
|
||||
const visible = !!label && (hover || show);
|
||||
return <React.Fragment>
|
||||
<input
|
||||
type="file"
|
||||
|
@ -82,11 +88,13 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
|
|||
>
|
||||
{ children }
|
||||
|
||||
<Tooltip
|
||||
label={label}
|
||||
visible={!!label && (hover || show)}
|
||||
forceOnRight
|
||||
/>
|
||||
<div className={classNames("mx_Tooltip", {
|
||||
"mx_Tooltip_visible": visible,
|
||||
"mx_Tooltip_invisible": !visible,
|
||||
})}>
|
||||
<div className="mx_Tooltip_chevron" />
|
||||
{ label }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import ResizeObserver from 'resize-observer-polyfill';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -61,6 +62,9 @@ export default class PersistedElement extends React.Component {
|
|||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
|
||||
// z-index for the element. Defaults to 9.
|
||||
zIndex: PropTypes.number,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -165,6 +169,7 @@ export default class PersistedElement extends React.Component {
|
|||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
Object.assign(child.style, {
|
||||
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
|
||||
position: 'absolute',
|
||||
top: parentRect.top + 'px',
|
||||
left: parentRect.left + 'px',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,23 +23,11 @@ import { Room, RoomMember } from 'matrix-js-sdk';
|
|||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+]).*?)(?=\/|\?|$)/;
|
||||
|
||||
class Pill extends React.Component {
|
||||
static isPillUrl(url) {
|
||||
return !!getPrimaryPermalinkEntity(url);
|
||||
}
|
||||
|
||||
static isMessagePillUrl(url) {
|
||||
return !!REGEX_LOCAL_PERMALINK.exec(url);
|
||||
}
|
||||
|
||||
static roomNotifPos(text) {
|
||||
return text.indexOf("@room");
|
||||
}
|
||||
|
@ -56,7 +44,7 @@ class Pill extends React.Component {
|
|||
static propTypes = {
|
||||
// The Type of this Pill. If url is given, this is auto-detected.
|
||||
type: PropTypes.string,
|
||||
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
|
||||
// The URL to pillify (no validation is done)
|
||||
url: PropTypes.string,
|
||||
// Whether the pill is in a message
|
||||
inMessage: PropTypes.bool,
|
||||
|
@ -90,12 +78,9 @@ class Pill extends React.Component {
|
|||
|
||||
if (nextProps.url) {
|
||||
if (nextProps.inMessage) {
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
const matrixToMatch = REGEX_LOCAL_PERMALINK.exec(nextProps.url) || [];
|
||||
|
||||
resourceId = matrixToMatch[1]; // The room/user ID
|
||||
prefix = matrixToMatch[2]; // The first character of prefix
|
||||
const parts = parseAppLocalLink(nextProps.url);
|
||||
resourceId = parts.primaryEntityId; // The room/user ID
|
||||
prefix = parts.sigil; // The first character of prefix
|
||||
} else {
|
||||
resourceId = getPrimaryPermalinkEntity(nextProps.url);
|
||||
prefix = resourceId ? resourceId[0] : undefined;
|
||||
|
|
|
@ -24,6 +24,7 @@ import {wantsDateSeparator} from '../../../DateUtils';
|
|||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {LayoutPropType} from "../../../settings/Layout";
|
||||
import escapeHtml from "escape-html";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
@ -42,7 +43,7 @@ export default class ReplyThread extends React.Component {
|
|||
onHeightChanged: PropTypes.func.isRequired,
|
||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
// Specifies which layout to use.
|
||||
useIRCLayout: PropTypes.bool,
|
||||
layout: LayoutPropType,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -209,7 +210,7 @@ export default class ReplyThread extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) {
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div className="mx_ReplyThread_wrapper_empty" />;
|
||||
}
|
||||
|
@ -218,7 +219,7 @@ export default class ReplyThread extends React.Component {
|
|||
onHeightChanged={onHeightChanged}
|
||||
ref={ref}
|
||||
permalinkCreator={permalinkCreator}
|
||||
useIRCLayout={useIRCLayout}
|
||||
layout={layout}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -386,7 +387,7 @@ export default class ReplyThread extends React.Component {
|
|||
permalinkCreator={this.props.permalinkCreator}
|
||||
isRedacted={ev.isRedacted()}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
replacingEventId={ev.replacingEventId()}
|
||||
/>
|
||||
|
|
|
@ -15,19 +15,40 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { chunk } from "lodash";
|
||||
import classNames from "classnames";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import classNames from "classnames";
|
||||
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||
idp: IIdentityProvider;
|
||||
mini?: boolean;
|
||||
}
|
||||
|
||||
const getIcon = (brand: IdentityProviderBrand | string) => {
|
||||
switch (brand) {
|
||||
case IdentityProviderBrand.Apple:
|
||||
return require(`../../../../res/img/element-icons/brands/apple.svg`);
|
||||
case IdentityProviderBrand.Facebook:
|
||||
return require(`../../../../res/img/element-icons/brands/facebook.svg`);
|
||||
case IdentityProviderBrand.Github:
|
||||
return require(`../../../../res/img/element-icons/brands/github.svg`);
|
||||
case IdentityProviderBrand.Gitlab:
|
||||
return require(`../../../../res/img/element-icons/brands/gitlab.svg`);
|
||||
case IdentityProviderBrand.Google:
|
||||
return require(`../../../../res/img/element-icons/brands/google.svg`);
|
||||
case IdentityProviderBrand.Twitter:
|
||||
return require(`../../../../res/img/element-icons/brands/twitter.svg`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
matrixClient,
|
||||
loginType,
|
||||
|
@ -37,7 +58,6 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
mini,
|
||||
...props
|
||||
}) => {
|
||||
const kind = primary ? "primary" : "primary_outline";
|
||||
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
|
||||
|
||||
const onClick = () => {
|
||||
|
@ -45,30 +65,35 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||
};
|
||||
|
||||
let icon;
|
||||
if (typeof idp?.icon === "string" && (idp.icon.startsWith("mxc://") || idp.icon.startsWith("https://"))) {
|
||||
icon = <img
|
||||
src={matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true)}
|
||||
height="24"
|
||||
width="24"
|
||||
alt={label}
|
||||
/>;
|
||||
let brandClass;
|
||||
const brandIcon = idp ? getIcon(idp.brand) : null;
|
||||
if (brandIcon) {
|
||||
const brandName = idp.brand.split(".").pop();
|
||||
brandClass = `mx_SSOButton_brand_${brandName}`;
|
||||
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
||||
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
|
||||
const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
|
||||
icon = <img src={src} height="24" width="24" alt={idp.name} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_SSOButton", {
|
||||
[brandClass]: brandClass,
|
||||
mx_SSOButton_mini: mini,
|
||||
mx_SSOButton_default: !idp,
|
||||
mx_SSOButton_primary: primary,
|
||||
});
|
||||
|
||||
if (mini) {
|
||||
// TODO fallback icon
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
<AccessibleTooltipButton {...props} title={label} className={classes} onClick={onClick}>
|
||||
{ icon }
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
|
||||
<AccessibleButton {...props} className={classes} onClick={onClick}>
|
||||
{ icon }
|
||||
{ label }
|
||||
</AccessibleButton>
|
||||
|
@ -83,6 +108,8 @@ interface IProps {
|
|||
primary?: boolean;
|
||||
}
|
||||
|
||||
const MAX_PER_ROW = 6;
|
||||
|
||||
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
|
||||
const providers = flow["org.matrix.msc2858.identity_providers"] || [];
|
||||
if (providers.length < 2) {
|
||||
|
@ -97,17 +124,24 @@ const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAf
|
|||
</div>;
|
||||
}
|
||||
|
||||
const rows = Math.ceil(providers.length / MAX_PER_ROW);
|
||||
const size = Math.ceil(providers.length / rows);
|
||||
|
||||
return <div className="mx_SSOButtons">
|
||||
{ providers.map(idp => (
|
||||
<SSOButton
|
||||
key={idp.id}
|
||||
matrixClient={matrixClient}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={idp}
|
||||
mini={true}
|
||||
primary={primary}
|
||||
/>
|
||||
{ chunk(providers, size).map(chunk => (
|
||||
<div key={chunk[0].id} className="mx_SSOButtons_row">
|
||||
{ chunk.map(idp => (
|
||||
<SSOButton
|
||||
key={idp.id}
|
||||
matrixClient={matrixClient}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
idp={idp}
|
||||
mini={true}
|
||||
primary={primary}
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
)) }
|
||||
</div>;
|
||||
};
|
||||
|
|
36
src/components/views/host_signup/HostSignupContainer.tsx
Normal file
36
src/components/views/host_signup/HostSignupContainer.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import HostSignupDialog from "../dialogs/HostSignupDialog";
|
||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
const HostSignupContainer = () => {
|
||||
const [isActive, setIsActive] = useState(HostSignupStore.instance.isHostSignupActive);
|
||||
useEventEmitter(HostSignupStore.instance, UPDATE_EVENT, () => {
|
||||
setIsActive(HostSignupStore.instance.isHostSignupActive);
|
||||
});
|
||||
|
||||
return <div className="mx_HostSignupContainer">
|
||||
{isActive &&
|
||||
<HostSignupDialog />
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default HostSignupContainer
|
|
@ -288,7 +288,7 @@ export default class MFileBody extends React.Component {
|
|||
<a ref={this._dummyLink} />
|
||||
</div>
|
||||
<iframe
|
||||
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
|
||||
src={url}
|
||||
onLoad={onIframeLoad}
|
||||
ref={this._iframe}
|
||||
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
|
||||
|
|
|
@ -362,7 +362,7 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
// The maximum height of the thumbnail as it is rendered as an <img>
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 240, infoHeight);
|
||||
// The maximum width of the thumbnail, as dictated by its natural
|
||||
// maximum height.
|
||||
const maxWidth = infoWidth * maxHeight / infoHeight;
|
||||
|
|
|
@ -19,6 +19,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -33,10 +35,15 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
|||
const url = this.props.mxEvent.getContent()['url'];
|
||||
const prevUrl = this.props.mxEvent.getPrevContent()['url'];
|
||||
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const widgetId = this.props.mxEvent.getStateKey();
|
||||
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find(w => w.id === widgetId);
|
||||
|
||||
let joinCopy = _t('Join the conference at the top of this room');
|
||||
if (!WidgetStore.instance.isPinned(this.props.mxEvent.getRoomId(), this.props.mxEvent.getStateKey())) {
|
||||
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
|
||||
joinCopy = _t('Join the conference from the room information card on the right');
|
||||
} else if (!widget) {
|
||||
joinCopy = null;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
|
|
|
@ -81,6 +81,7 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
|
||||
_applyFormatting() {
|
||||
const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers");
|
||||
this.activateSpoilers([this._content.current]);
|
||||
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
|
@ -91,29 +92,140 @@ export default class TextualBody extends React.Component {
|
|||
this.calculateUrlPreview();
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
|
||||
const blocks = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
if (blocks.length > 0) {
|
||||
// Handle expansion and add buttons
|
||||
const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
|
||||
if (pres.length > 0) {
|
||||
for (let i = 0; i < pres.length; i++) {
|
||||
// If there already is a div wrapping the codeblock we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = this._wrapInDiv(pres[i]);
|
||||
this._handleCodeBlockExpansion(pres[i]);
|
||||
this._addCodeExpansionButton(div, pres[i]);
|
||||
this._addCodeCopyButton(div);
|
||||
if (showLineNumbers) {
|
||||
this._addLineNumbers(pres[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Highlight code
|
||||
const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
if (codes.length > 0) {
|
||||
// Do this asynchronously: parsing code takes time and we don't
|
||||
// need to block the DOM update on it.
|
||||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
const classes = blocks[i].className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
// If the code already has the hljs class we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (codes[i].className.includes("hljs")) continue;
|
||||
this._highlightCode(codes[i]);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
this._addCodeCopyButton();
|
||||
}
|
||||
}
|
||||
|
||||
_addCodeExpansionButton(div, pre) {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button ";
|
||||
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
|
||||
button.className += "mx_EventTile_expandButton";
|
||||
} else {
|
||||
button.className += "mx_EventTile_collapseButton";
|
||||
}
|
||||
|
||||
button.onclick = async () => {
|
||||
button.className = "mx_EventTile_button ";
|
||||
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
|
||||
pre.className = "";
|
||||
button.className += "mx_EventTile_collapseButton";
|
||||
} else {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
button.className += "mx_EventTile_expandButton";
|
||||
}
|
||||
|
||||
// By expanding/collapsing we changed
|
||||
// the height, therefore we call this
|
||||
this.props.onHeightChanged();
|
||||
};
|
||||
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_addCodeCopyButton(div) {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button mx_EventTile_copyButton ";
|
||||
|
||||
// Check if expansion button exists. If so
|
||||
// we put the copy button to the bottom
|
||||
const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button");
|
||||
if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
|
||||
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("code")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
button.onmouseleave = close;
|
||||
};
|
||||
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_wrapInDiv(pre) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
pre.parentNode.replaceChild(div, pre);
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(pre);
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
_handleCodeBlockExpansion(pre) {
|
||||
if (!SettingsStore.getValue("expandCodeByDefault")) {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
}
|
||||
}
|
||||
|
||||
_addLineNumbers(pre) {
|
||||
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
|
||||
const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
|
||||
// Calculate number of lines in pre
|
||||
const number = pre.innerHTML.split(/\n/).length;
|
||||
// Iterate through lines starting with 1 (number of the first line is 1)
|
||||
for (let i = 1; i < number; i++) {
|
||||
lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
_highlightCode(code) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(code);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
const classes = code.className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,38 +366,6 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_addCodeCopyButton() {
|
||||
// Add 'copy' buttons to pre blocks
|
||||
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_copyButton";
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
button.onmouseleave = close;
|
||||
};
|
||||
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
p.parentNode.replaceChild(div, p);
|
||||
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(p);
|
||||
div.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
onCancelClick = event => {
|
||||
this.setState({ widgetHidden: true });
|
||||
// FIXME: persist this somewhere smarter than local storage
|
||||
|
@ -410,7 +490,10 @@ export default class TextualBody extends React.Component {
|
|||
ref: this._content,
|
||||
});
|
||||
if (this.props.replacingEventId) {
|
||||
body = [body, this._renderEditedMarker()];
|
||||
body = <>
|
||||
{body}
|
||||
{this._renderEditedMarker()}
|
||||
</>;
|
||||
}
|
||||
|
||||
if (this.props.highlightLink) {
|
||||
|
|
|
@ -33,6 +33,7 @@ interface IProps {
|
|||
previousPhase?: RightPanelPhases;
|
||||
closeLabel?: string;
|
||||
onClose?(): void;
|
||||
refireParams?;
|
||||
}
|
||||
|
||||
interface IGroupProps {
|
||||
|
@ -56,6 +57,7 @@ const BaseCard: React.FC<IProps> = ({
|
|||
withoutScrollContainer,
|
||||
previousPhase,
|
||||
children,
|
||||
refireParams,
|
||||
}) => {
|
||||
let backButton;
|
||||
if (previousPhase) {
|
||||
|
@ -63,6 +65,7 @@ const BaseCard: React.FC<IProps> = ({
|
|||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: previousPhase,
|
||||
refireParams: refireParams,
|
||||
});
|
||||
};
|
||||
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />;
|
||||
|
|
|
@ -37,13 +37,14 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import WidgetStore, {IApp, MAX_PINNED} from "../../../stores/WidgetStore";
|
||||
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -76,8 +77,9 @@ export const useWidgets = (room: Room) => {
|
|||
setApps([...WidgetStore.instance.getApps(room.roomId)]);
|
||||
}, [room]);
|
||||
|
||||
useEffect(updateApps, [room]);
|
||||
useEffect(updateApps, [room, updateApps]);
|
||||
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
|
||||
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
@ -102,10 +104,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const isPinned = WidgetStore.instance.isPinned(room.roomId, app.id);
|
||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
const togglePin = isPinned
|
||||
? () => { WidgetStore.instance.unpinWidget(room.roomId, app.id); }
|
||||
: () => { WidgetStore.instance.pinWidget(room.roomId, app.id); };
|
||||
? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); }
|
||||
: () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); };
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
let contextMenu;
|
||||
|
@ -120,7 +122,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||
/>;
|
||||
}
|
||||
|
||||
const cannotPin = !isPinned && !WidgetStore.instance.canPin(room.roomId, app.id);
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
||||
|
||||
let pinTitle: string;
|
||||
if (cannotPin) {
|
||||
|
@ -184,9 +186,18 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
|||
}
|
||||
};
|
||||
|
||||
let copyLayoutBtn = null;
|
||||
if (apps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
|
||||
copyLayoutBtn = (
|
||||
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
|
||||
{ _t("Set my room layout for everyone") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||
{ apps.map(app => <AppRow key={app.id} app={app} room={room} />) }
|
||||
|
||||
{ copyLayoutBtn }
|
||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -60,6 +60,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
|||
import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
|
||||
interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -1534,6 +1535,24 @@ const UserInfo: React.FC<Props> = ({
|
|||
|
||||
const classes = ["mx_UserInfo"];
|
||||
|
||||
let refireParams;
|
||||
let previousPhase: RightPanelPhases;
|
||||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
if (room && phase === RightPanelPhases.EncryptionPanel) {
|
||||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||
refireParams = {member: member};
|
||||
} else if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
const onEncryptionPanelClose = () => {
|
||||
dis.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: previousPhase,
|
||||
refireParams: refireParams,
|
||||
});
|
||||
}
|
||||
|
||||
let content;
|
||||
switch (phase) {
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
|
@ -1553,19 +1572,13 @@ const UserInfo: React.FC<Props> = ({
|
|||
<EncryptionPanel
|
||||
{...props as React.ComponentProps<typeof EncryptionPanel>}
|
||||
member={member}
|
||||
onClose={onClose}
|
||||
onClose={onEncryptionPanelClose}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let previousPhase: RightPanelPhases;
|
||||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
let closeLabel = undefined;
|
||||
if (phase === RightPanelPhases.EncryptionPanel) {
|
||||
const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest;
|
||||
|
@ -1581,6 +1594,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
onClose={onClose}
|
||||
closeLabel={closeLabel}
|
||||
previousPhase={previousPhase}
|
||||
refireParams={refireParams}
|
||||
>
|
||||
{ content }
|
||||
</BaseCard>;
|
||||
|
|
|
@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useEffect} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import BaseCard from "./BaseCard";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {useWidgets} from "./RoomSummaryCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useWidgets } from "./RoomSummaryCard";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -42,7 +42,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||
|
||||
const apps = useWidgets(room);
|
||||
const app = apps.find(a => a.id === widgetId);
|
||||
const isPinned = app && WidgetStore.instance.isPinned(room.roomId, app.id);
|
||||
const isPinned = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
|
|
|
@ -69,19 +69,24 @@ export default class RoomProfileSettings extends React.Component {
|
|||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
_cancelProfileChanges = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
displayName: this.state.originalDisplayName,
|
||||
topic: this.state.originalTopic,
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
avatarFile: null,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
|
@ -108,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: undefined}, '');
|
||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
|
||||
}
|
||||
|
||||
if (this.state.originalTopic !== this.state.topic) {
|
||||
|
@ -120,17 +125,21 @@ export default class RoomProfileSettings extends React.Component {
|
|||
};
|
||||
|
||||
_onDisplayNameChanged = (e) => {
|
||||
this.setState({
|
||||
displayName: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
this.setState({displayName: e.target.value});
|
||||
if (this.state.originalDisplayName === e.target.value) {
|
||||
this.setState({enableProfileSave: false});
|
||||
} else {
|
||||
this.setState({enableProfileSave: true});
|
||||
}
|
||||
};
|
||||
|
||||
_onTopicChanged = (e) => {
|
||||
this.setState({
|
||||
topic: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
this.setState({topic: e.target.value});
|
||||
if (this.state.originalTopic === e.target.value) {
|
||||
this.setState({enableProfileSave: false});
|
||||
} else {
|
||||
this.setState({enableProfileSave: true});
|
||||
}
|
||||
};
|
||||
|
||||
_onAvatarChanged = (e) => {
|
||||
|
@ -158,6 +167,33 @@ export default class RoomProfileSettings extends React.Component {
|
|||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
|
||||
let profileSettingsButtons;
|
||||
if (
|
||||
this.state.canSetName ||
|
||||
this.state.canSetTopic ||
|
||||
this.state.canSetAvatar
|
||||
) {
|
||||
profileSettingsButtons = (
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
|
@ -172,7 +208,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
<Field label={_t("Room Name")}
|
||||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} />
|
||||
<Field id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
|
||||
<Field className="mx_ProfileSettings_controls_topic" id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
|
||||
type="text" value={this.state.topic} autoComplete="off"
|
||||
onChange={this._onTopicChanged} element="textarea" />
|
||||
</div>
|
||||
|
@ -183,22 +219,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{ profileSettingsButtons }
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,12 +28,13 @@ import WidgetUtils from '../../../utils/WidgetUtils';
|
|||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer from "../../../resizer/resizer";
|
||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
|
||||
import {useStateCallback} from "../../../hooks/useStateCallback";
|
||||
|
||||
export default class AppsDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -62,13 +63,13 @@ export default class AppsDrawer extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
ScalarMessaging.startListening();
|
||||
WidgetStore.instance.on(this.props.room.roomId, this._updateApps);
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ScalarMessaging.stopListening();
|
||||
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
if (this._resizeContainer) {
|
||||
this.resizer.detach();
|
||||
|
@ -102,11 +103,10 @@ export default class AppsDrawer extends React.Component {
|
|||
},
|
||||
onResizeStop: () => {
|
||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
// persist to localStorage
|
||||
localStorage.setItem(this._getStorageKey(), JSON.stringify([
|
||||
this.state.apps.map(app => app.id),
|
||||
...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
]));
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room, Container.Top,
|
||||
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
);
|
||||
},
|
||||
};
|
||||
// pass a truthy container for now, we won't call attach until we update it
|
||||
|
@ -128,8 +128,6 @@ export default class AppsDrawer extends React.Component {
|
|||
this._loadResizerPreferences();
|
||||
};
|
||||
|
||||
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
|
||||
|
||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -147,24 +145,16 @@ export default class AppsDrawer extends React.Component {
|
|||
};
|
||||
|
||||
_loadResizerPreferences = () => {
|
||||
try {
|
||||
const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
|
||||
// Every app was included in the last split, reuse the last sizes
|
||||
if (this.state.apps.length <= lastIds.length && this.state.apps.every((app, i) => lastIds[i] === app.id)) {
|
||||
sizes.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// this is expected
|
||||
}
|
||||
|
||||
if (this.state.apps) {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
} else if (this.state.apps) {
|
||||
const distributors = this.resizer.getDistributors();
|
||||
distributors.forEach(d => d.item.clearSize());
|
||||
distributors.forEach(d => d.start());
|
||||
|
@ -190,7 +180,7 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
|
||||
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
|
||||
_updateApps = () => {
|
||||
this.setState({
|
||||
|
@ -248,10 +238,11 @@ export default class AppsDrawer extends React.Component {
|
|||
return (
|
||||
<div className={classes}>
|
||||
<PersistentVResizer
|
||||
id={"apps-drawer_" + this.props.room.roomId}
|
||||
room={this.props.room}
|
||||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
|
@ -272,7 +263,7 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
|
||||
const PersistentVResizer = ({
|
||||
id,
|
||||
room,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
className,
|
||||
|
@ -281,7 +272,24 @@ const PersistentVResizer = ({
|
|||
resizeNotifier,
|
||||
children,
|
||||
}) => {
|
||||
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
|
||||
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
|
||||
|
||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||
if (!minHeight) minHeight = 100;
|
||||
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
|
||||
|
||||
// Convert from percentage to height. Note that the default height is 280px.
|
||||
if (defaultHeight) {
|
||||
defaultHeight = clamp(defaultHeight, 0, 100);
|
||||
defaultHeight = percentageWithin(defaultHeight / 100, minHeight, maxHeight);
|
||||
} else {
|
||||
defaultHeight = 280;
|
||||
}
|
||||
|
||||
const [height, setHeight] = useStateCallback(defaultHeight, newHeight => {
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
});
|
||||
|
||||
return <Resizable
|
||||
size={{height: Math.min(height, maxHeight)}}
|
||||
|
|
|
@ -519,7 +519,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private async tabCompleteName(event: React.KeyboardEvent) {
|
||||
try {
|
||||
await new Promise(resolve => this.setState({showVisualBell: false}, resolve));
|
||||
await new Promise<void>(resolve => this.setState({showVisualBell: false}, resolve));
|
||||
const {model} = this.props;
|
||||
const caret = this.getCaret();
|
||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
|
|
|
@ -27,6 +27,7 @@ import * as TextForEvent from "../../../TextForEvent";
|
|||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout, LayoutPropType} from "../../../settings/Layout";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {formatTime} from "../../../DateUtils";
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
@ -37,6 +38,7 @@ import {E2E_STATE} from "./E2EIcon";
|
|||
import {toRem} from "../../../utils/units";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
|
@ -65,6 +67,7 @@ const stateEventTileTypes = {
|
|||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
|
||||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
|
@ -225,8 +228,8 @@ export default class EventTile extends React.Component {
|
|||
// whether to show reactions for this event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
|
||||
// whether or not to show flair at all
|
||||
enableFlair: PropTypes.bool,
|
||||
|
@ -732,7 +735,7 @@ export default class EventTile extends React.Component {
|
|||
// joins/parts/etc
|
||||
avatarSize = 14;
|
||||
needsSenderProfile = false;
|
||||
} else if (this.props.useIRCLayout) {
|
||||
} else if (this.props.layout == Layout.IRC) {
|
||||
avatarSize = 14;
|
||||
needsSenderProfile = true;
|
||||
} else if (this.props.continuation && this.props.tileShape !== "file_grid") {
|
||||
|
@ -843,10 +846,11 @@ export default class EventTile extends React.Component {
|
|||
{ timestamp }
|
||||
</a>;
|
||||
|
||||
const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null;
|
||||
const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null;
|
||||
const groupPadlock = !this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
const ircPadlock = this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
const useIRCLayout = this.props.layout == Layout.IRC;
|
||||
const groupTimestamp = !useIRCLayout ? linkedTimestamp : null;
|
||||
const ircTimestamp = useIRCLayout ? linkedTimestamp : null;
|
||||
const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
|
@ -941,7 +945,7 @@ export default class EventTile extends React.Component {
|
|||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this._replyThread,
|
||||
this.props.useIRCLayout,
|
||||
this.props.layout,
|
||||
);
|
||||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
|
|
|
@ -107,7 +107,7 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
if (!SettingsStore.getValue("showImages")) {
|
||||
image = null; // Don't render a button to show the image, just hide it outright
|
||||
}
|
||||
const imageMaxWidth = 100; const imageMaxHeight = 100;
|
||||
const imageMaxWidth = 320; const imageMaxHeight = 240;
|
||||
if (image && image.startsWith("mxc://")) {
|
||||
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
|
||||
}
|
||||
|
@ -134,6 +134,10 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_LinkPreviewWidget">
|
||||
<AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
|
||||
<img className="mx_filterFlipColor" alt="" role="presentation"
|
||||
src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
{ img }
|
||||
<div className="mx_LinkPreviewWidget_caption">
|
||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
|
||||
|
@ -142,10 +146,6 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
{ description }
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
|
||||
<img className="mx_filterFlipColor" alt="" role="presentation"
|
||||
src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -109,9 +109,12 @@ function HangupButton(props) {
|
|||
|
||||
dis.dispatch({
|
||||
action,
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId,
|
||||
// hangup the call for this room. NB. We use the room in props as the room ID
|
||||
// as call.roomId may be the 'virtual room', and the dispatch actions always
|
||||
// use the user-facing room (there was a time when we deliberately used
|
||||
// call.roomId and *not* props.roomId, but that was for the old
|
||||
// style Freeswitch conference calls and those times are gone.)
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -423,7 +426,8 @@ export default class MessageComposer extends React.Component {
|
|||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Widgets)) {
|
||||
if (SettingsStore.getValue(UIFeature.Widgets) &&
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
|
||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
appear={true} in={this.state.doAnimation} timeout={640}
|
||||
classNames='mx_RoomBreadcrumbs'
|
||||
>
|
||||
<Toolbar className='mx_RoomBreadcrumbs'>
|
||||
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
|
||||
{tiles.slice(this.state.skipFirst ? 1 : 0)}
|
||||
</Toolbar>
|
||||
</CSSTransition>
|
||||
|
|
|
@ -46,6 +46,7 @@ import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
|||
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -89,10 +90,44 @@ interface ITagAesthetics {
|
|||
defaultHidden: boolean;
|
||||
}
|
||||
|
||||
const TAG_AESTHETICS: {
|
||||
interface ITagAestheticsMap {
|
||||
// @ts-ignore - TS wants this to be a string but we know better
|
||||
[tagId: TagID]: ITagAesthetics;
|
||||
} = {
|
||||
}
|
||||
|
||||
// If we have no dialer support, we just show the create chat dialog
|
||||
const dmOnAddRoom = (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
|
||||
};
|
||||
|
||||
// If we have dialer support, show a context menu so the user can pick between
|
||||
// the dialer and the create chat dialog
|
||||
const dmAddRoomContextMenu = (onFinished: () => void) => {
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start a Conversation")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.dispatch({action: "view_create_chat"});
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Open dial pad")}
|
||||
iconClassName="mx_RoomList_iconDialpad"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.fire(Action.OpenDialPad);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
};
|
||||
|
||||
const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||
[DefaultTagID.Invite]: {
|
||||
sectionLabel: _td("Invites"),
|
||||
isInvite: true,
|
||||
|
@ -108,9 +143,8 @@ const TAG_AESTHETICS: {
|
|||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Start chat"),
|
||||
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
|
||||
},
|
||||
// Either onAddRoom or addRoomContextMenu are set depending on whether we
|
||||
// have dialer support.
|
||||
},
|
||||
[DefaultTagID.Untagged]: {
|
||||
sectionLabel: _td("Rooms"),
|
||||
|
@ -178,6 +212,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
|||
export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
private tagAesthetics: ITagAestheticsMap;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -187,6 +222,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
||||
};
|
||||
|
||||
// shallow-copy from the template as we need to make modifications to it
|
||||
this.tagAesthetics = objectShallowClone(TAG_AESTHETICS);
|
||||
this.updateDmAddRoomAction();
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
}
|
||||
|
||||
|
@ -202,6 +241,17 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
if (this.customTagStoreRef) this.customTagStoreRef.remove();
|
||||
}
|
||||
|
||||
private updateDmAddRoomAction() {
|
||||
const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
|
||||
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||
dmTagAesthetics.addRoomContextMenu = dmAddRoomContextMenu;
|
||||
} else {
|
||||
dmTagAesthetics.onAddRoom = dmOnAddRoom;
|
||||
}
|
||||
|
||||
this.tagAesthetics[DefaultTagID.DM] = dmTagAesthetics;
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.ViewRoomDelta) {
|
||||
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
|
||||
|
@ -214,6 +264,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||
});
|
||||
}
|
||||
} else if (payload.action === Action.PstnSupportUpdated) {
|
||||
this.updateDmAddRoomAction();
|
||||
this.updateLists();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -355,7 +408,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
|
||||
? customTagAesthetics(orderedTagId)
|
||||
: TAG_AESTHETICS[orderedTagId];
|
||||
: this.tagAesthetics[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
components.push(<RoomSublist
|
||||
|
@ -402,8 +455,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
||||
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
|
||||
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
||||
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
||||
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) {
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
|
||||
<AccessibleButton
|
||||
|
|
|
@ -29,7 +29,7 @@ import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
@ -51,7 +51,6 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -99,12 +98,23 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
MessagePreviewStore.instance.on(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.on("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room) => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
private onNotificationUpdate = () => {
|
||||
|
@ -128,6 +138,26 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
|
||||
this.setState({messagePreview: this.generatePreview()});
|
||||
}
|
||||
if (prevProps.room?.roomId !== this.props.room?.roomId) {
|
||||
MessagePreviewStore.instance.off(
|
||||
MessagePreviewStore.getPreviewChangedEventName(prevProps.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
MessagePreviewStore.instance.on(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(prevProps.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
prevProps.room?.off("Room.name", this.onRoomNameUpdate);
|
||||
this.props.room?.on("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -140,11 +170,18 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
if (this.props.room) {
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
MessagePreviewStore.instance.off(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
|
|
|
@ -156,13 +156,14 @@ export default class SendMessageComposer extends React.Component {
|
|||
this.onVerticalArrow(event, true);
|
||||
} else if (event.key === Key.ARROW_DOWN) {
|
||||
this.onVerticalArrow(event, false);
|
||||
} else if (this._prepareToEncrypt) {
|
||||
this._prepareToEncrypt();
|
||||
} else if (event.key === Key.ESCAPE) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
});
|
||||
} else if (this._prepareToEncrypt) {
|
||||
// This needs to be last!
|
||||
this._prepareToEncrypt();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -402,6 +403,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
this._editorRef.clearUndoHistory();
|
||||
this._editorRef.focus();
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "scroll_to_bottom"});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -264,7 +264,7 @@ export default class Stickerpicker extends React.Component {
|
|||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}>
|
||||
<AppTile
|
||||
app={stickerApp}
|
||||
room={this.props.room}
|
||||
|
|
|
@ -81,7 +81,7 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
};
|
||||
|
||||
onRoomTimeline = (event, room) => {
|
||||
if (room && room.roomId === this.props.room.roomId) {
|
||||
if (room?.roomId === this.props.room?.roomId) {
|
||||
const userId = event.getSender();
|
||||
// remove user from usersTyping
|
||||
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
|
||||
|
|
|
@ -65,7 +65,7 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
|||
|
||||
const avatarClasses = classNames({
|
||||
"mx_AvatarSetting_avatar": true,
|
||||
"mx_AvatarSetting_avatar_hovering": isHovering,
|
||||
"mx_AvatarSetting_avatar_hovering": isHovering && uploadAvatar,
|
||||
});
|
||||
return <div className={avatarClasses}>
|
||||
{avatarElement}
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Pill from "../elements/Pill";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
@replaceableComponent("views.settings.BridgeTile")
|
||||
export default class BridgeTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
ev: PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
visible: false,
|
||||
}
|
||||
|
||||
_toggleVisible() {
|
||||
this.setState({
|
||||
visible: !this.state.visible,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.props.ev.getContent();
|
||||
const { channel, network, protocol } = content;
|
||||
const protocolName = protocol.displayname || protocol.id;
|
||||
const channelName = channel.displayname || channel.id;
|
||||
const networkName = network ? network.displayname || network.id : protocolName;
|
||||
|
||||
let creator = null;
|
||||
if (content.creator) {
|
||||
creator = _t("This bridge was provisioned by <user />.", {}, {
|
||||
user: <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
}
|
||||
|
||||
const bot = _t("This bridge is managed by <user />.", {}, {
|
||||
user: <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(this.props.ev.getSender())}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
|
||||
let networkIcon;
|
||||
|
||||
if (protocol.avatar) {
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
protocol.avatar, 64, 64, "crop",
|
||||
);
|
||||
|
||||
networkIcon = <BaseAvatar className="protocol-icon"
|
||||
width={48}
|
||||
height={48}
|
||||
resizeMethod='crop'
|
||||
name={ protocolName }
|
||||
idName={ protocolName }
|
||||
url={ avatarUrl }
|
||||
/>;
|
||||
} else {
|
||||
networkIcon = <div class="noProtocolIcon"></div>;
|
||||
}
|
||||
|
||||
const id = this.props.ev.getId();
|
||||
const metadataClassname = "metadata" + (this.state.visible ? " visible" : "");
|
||||
return (<li key={id}>
|
||||
<div className="column-icon">
|
||||
{networkIcon}
|
||||
</div>
|
||||
<div className="column-data">
|
||||
<h3>{protocolName}</h3>
|
||||
<p className="workspace-channel-details">
|
||||
<span>{_t("Workspace: %(networkName)s", {networkName})}</span>
|
||||
<span className="channel">{_t("Channel: %(channelName)s", {channelName})}</span>
|
||||
</p>
|
||||
<p className={metadataClassname}>
|
||||
{creator} {bot}
|
||||
</p>
|
||||
<AccessibleButton className="mx_showMore" kind="secondary" onClick={this._toggleVisible.bind(this)}>
|
||||
{ this.state.visible ? _t("Show less") : _t("Show more") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</li>);
|
||||
}
|
||||
}
|
167
src/components/views/settings/BridgeTile.tsx
Normal file
167
src/components/views/settings/BridgeTile.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Pill from "../elements/Pill";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
|
||||
interface IProps {
|
||||
ev: MatrixEvent;
|
||||
room: Room;
|
||||
}
|
||||
|
||||
/**
|
||||
* This should match https://github.com/matrix-org/matrix-doc/blob/hs/msc-bridge-inf/proposals/2346-bridge-info-state-event.md#mbridge
|
||||
*/
|
||||
interface IBridgeStateEvent {
|
||||
bridgebot: string;
|
||||
creator?: string;
|
||||
protocol: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
network?: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
channel: {
|
||||
id: string;
|
||||
displayname?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
external_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class BridgeTile extends React.PureComponent<IProps> {
|
||||
static propTypes = {
|
||||
ev: PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const content: IBridgeStateEvent = this.props.ev.getContent();
|
||||
// Validate
|
||||
if (!content.channel?.id || !content.protocol?.id) {
|
||||
console.warn(`Bridge info event ${this.props.ev.getId()} has missing content. Tile will not render`);
|
||||
return null;
|
||||
}
|
||||
if (!content.bridgebot) {
|
||||
// Bridgebot was not required previously, so in order to not break rooms we are allowing
|
||||
// the sender to be used in place. When the proposal is merged, this should be removed.
|
||||
console.warn(`Bridge info event ${this.props.ev.getId()} does not provide a 'bridgebot' key which`
|
||||
+ "is deprecated behaviour. Using sender for now.");
|
||||
content.bridgebot = this.props.ev.getSender();
|
||||
}
|
||||
const { channel, network, protocol } = content;
|
||||
const protocolName = protocol.displayname || protocol.id;
|
||||
const channelName = channel.displayname || channel.id;
|
||||
|
||||
let creator = null;
|
||||
if (content.creator) {
|
||||
creator = <li>{_t("This bridge was provisioned by <user />.", {}, {
|
||||
user: () => <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
})}</li>;
|
||||
}
|
||||
|
||||
const bot = <li>{_t("This bridge is managed by <user />.", {}, {
|
||||
user: () => <Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.bridgebot)}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
})}</li>;
|
||||
|
||||
let networkIcon;
|
||||
|
||||
if (protocol.avatar_url) {
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
protocol.avatar_url, 64, 64, "crop",
|
||||
);
|
||||
|
||||
networkIcon = <BaseAvatar className="protocol-icon"
|
||||
width={48}
|
||||
height={48}
|
||||
resizeMethod='crop'
|
||||
name={ protocolName }
|
||||
idName={ protocolName }
|
||||
url={ avatarUrl }
|
||||
/>;
|
||||
} else {
|
||||
networkIcon = <div className="noProtocolIcon"></div>;
|
||||
}
|
||||
let networkItem = null;
|
||||
if (network) {
|
||||
const networkName = network.displayname || network.id;
|
||||
let networkLink = <span>{networkName}</span>;
|
||||
if (typeof network.external_url === "string" && isUrlPermitted(network.external_url)) {
|
||||
networkLink = <a href={network.external_url} target="_blank" rel="noreferrer noopener">{networkName}</a>
|
||||
}
|
||||
networkItem = _t("Workspace: <networkLink/>", {}, {
|
||||
networkLink: () => networkLink,
|
||||
});
|
||||
}
|
||||
|
||||
let channelLink = <span>{channelName}</span>;
|
||||
if (typeof channel.external_url === "string" && isUrlPermitted(channel.external_url)) {
|
||||
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{channelName}</a>
|
||||
}
|
||||
|
||||
const id = this.props.ev.getId();
|
||||
return (<li key={id}>
|
||||
<div className="column-icon">
|
||||
{networkIcon}
|
||||
</div>
|
||||
<div className="column-data">
|
||||
<h3>{protocolName}</h3>
|
||||
<p className="workspace-channel-details">
|
||||
{networkItem}
|
||||
<span className="channel">{_t("Channel: <channelLink/>", {}, {
|
||||
channelLink: () => channelLink,
|
||||
})}</span>
|
||||
</p>
|
||||
<ul className="metadata">
|
||||
{creator} {bot}
|
||||
</ul>
|
||||
</div>
|
||||
</li>);
|
||||
}
|
||||
}
|
|
@ -52,19 +52,23 @@ export default class ProfileSettings extends React.Component {
|
|||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
_cancelProfileChanges = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
displayName: this.state.originalDisplayName,
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
avatarFile: null,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
|
@ -186,7 +190,7 @@ export default class ProfileSettings extends React.Component {
|
|||
</div>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
onClick={this._cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
|
|
|
@ -424,7 +424,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
<p>{_t(
|
||||
"Back up your encryption keys with your account data in case you " +
|
||||
"lose access to your sessions. Your keys will be secured with a " +
|
||||
"unique Recovery Key.",
|
||||
"unique Security Key.",
|
||||
)}</p>
|
||||
{statusDescription}
|
||||
<details>
|
||||
|
|
|
@ -28,15 +28,14 @@ import { FontWatcher } from "../../../../../settings/watchers/FontWatcher";
|
|||
import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload';
|
||||
import { Action } from '../../../../../dispatcher/actions';
|
||||
import { IValidationResult, IFieldState } from '../../../elements/Validation';
|
||||
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||
import StyledCheckbox from '../../../elements/StyledCheckbox';
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import Field from '../../../elements/Field';
|
||||
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||
import classNames from 'classnames';
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
import {Layout} from "../../../../../settings/Layout";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -62,7 +61,7 @@ interface IState extends IThemeState {
|
|||
useSystemFont: boolean;
|
||||
systemFont: string;
|
||||
showAdvanced: boolean;
|
||||
useIRCLayout: boolean;
|
||||
layout: Layout;
|
||||
}
|
||||
|
||||
|
||||
|
@ -83,7 +82,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
useSystemFont: SettingsStore.getValue("useSystemFont"),
|
||||
systemFont: SettingsStore.getValue("systemFont"),
|
||||
showAdvanced: false,
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -213,15 +212,15 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
this.setState({customThemeUrl: e.target.value});
|
||||
};
|
||||
|
||||
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const val = e.target.value === "true";
|
||||
|
||||
this.setState({
|
||||
useIRCLayout: val,
|
||||
});
|
||||
|
||||
SettingsStore.setValue("useIRCLayout", null, SettingLevel.DEVICE, val);
|
||||
};
|
||||
private onIRCLayoutChange = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
this.setState({layout: Layout.IRC});
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
} else {
|
||||
this.setState({layout: Layout.Group});
|
||||
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||
}
|
||||
}
|
||||
|
||||
private renderThemeSection() {
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
|
@ -306,7 +305,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_fontSlider_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
layout={this.state.layout}
|
||||
/>
|
||||
<div className="mx_AppearanceUserSettingsTab_fontSlider">
|
||||
<div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
|
||||
|
@ -342,50 +341,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</div>;
|
||||
}
|
||||
|
||||
private renderLayoutSection = () => {
|
||||
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Message layout")}</span>
|
||||
|
||||
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
||||
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.useIRCLayout,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={true}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="true"
|
||||
checked={this.state.useIRCLayout}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{_t("Compact")}
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
<div className="mx_AppearanceUserSettingsTab_spacer" />
|
||||
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: !this.state.useIRCLayout,
|
||||
})}>
|
||||
<EventTilePreview
|
||||
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||
message={this.MESSAGE_PREVIEW_TEXT}
|
||||
useIRCLayout={false}
|
||||
/>
|
||||
<StyledRadioButton
|
||||
name="layout"
|
||||
value="false"
|
||||
checked={!this.state.useIRCLayout}
|
||||
onChange={this.onLayoutChange}
|
||||
>
|
||||
{_t("Modern")}
|
||||
</StyledRadioButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
private renderAdvancedSection() {
|
||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||
|
||||
|
@ -409,14 +364,15 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
name="useCompactLayout"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
disabled={this.state.useIRCLayout}
|
||||
/>
|
||||
<SettingsFlag
|
||||
name="useIRCLayout"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
onChange={(checked) => this.setState({useIRCLayout: checked})}
|
||||
disabled={this.state.layout == Layout.IRC}
|
||||
/>
|
||||
<StyledCheckbox
|
||||
checked={this.state.layout == Layout.IRC}
|
||||
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
|
||||
>
|
||||
{_t("Enable experimental, compact IRC style layout")}
|
||||
</StyledCheckbox>
|
||||
|
||||
<SettingsFlag
|
||||
name="useSystemFont"
|
||||
level={SettingLevel.DEVICE}
|
||||
|
|
|
@ -34,6 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'MessageComposerInput.suggestEmoji',
|
||||
'sendTypingNotifications',
|
||||
'MessageComposerInput.ctrlEnterToSend',
|
||||
'MessageComposerInput.showStickersButton',
|
||||
];
|
||||
|
||||
static TIMELINE_SETTINGS = [
|
||||
|
@ -46,12 +47,15 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'alwaysShowTimestamps',
|
||||
'showRedactions',
|
||||
'enableSyntaxHighlightLanguageDetection',
|
||||
'expandCodeByDefault',
|
||||
'showCodeLineNumbers',
|
||||
'showJoinLeaves',
|
||||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
'showChatEffects',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
'ctrlFForSearch',
|
||||
];
|
||||
|
||||
static GENERAL_SETTINGS = [
|
||||
|
|
|
@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, CSSProperties, ReactNode } from 'react';
|
||||
import React, { createRef, CSSProperties } from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import VideoFeed, { VideoFeedType } from "./VideoFeed";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
|
@ -27,9 +27,10 @@ import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
|||
import classNames from 'classnames';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
||||
import {aboveLeftOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu';
|
||||
import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu';
|
||||
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||
import { avatarUrlForMember } from '../../../Avatar';
|
||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||
|
||||
interface IProps {
|
||||
// The call for us to display
|
||||
|
@ -60,6 +61,7 @@ interface IState {
|
|||
callState: CallState,
|
||||
controlsVisible: boolean,
|
||||
showMoreMenu: boolean,
|
||||
showDialpad: boolean,
|
||||
}
|
||||
|
||||
function getFullScreenElement() {
|
||||
|
@ -102,6 +104,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
private dispatcherRef: string;
|
||||
private contentRef = createRef<HTMLDivElement>();
|
||||
private controlsHideTimer: number = null;
|
||||
private dialpadButton = createRef<HTMLDivElement>();
|
||||
private contextMenuButton = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
|
@ -115,6 +118,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
callState: this.props.call.state,
|
||||
controlsVisible: true,
|
||||
showMoreMenu: false,
|
||||
showDialpad: false,
|
||||
}
|
||||
|
||||
this.updateCallListeners(null, this.props.call);
|
||||
|
@ -208,9 +212,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onExpandClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -226,7 +231,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private showControls() {
|
||||
if (this.state.showMoreMenu) return;
|
||||
if (this.state.showMoreMenu || this.state.showDialpad) return;
|
||||
|
||||
if (!this.state.controlsVisible) {
|
||||
this.setState({
|
||||
|
@ -239,6 +244,29 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
|
||||
private onDialpadClick = () => {
|
||||
if (!this.state.showDialpad) {
|
||||
if (this.controlsHideTimer) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
this.controlsHideTimer = null;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showDialpad: true,
|
||||
controlsVisible: true,
|
||||
});
|
||||
} else {
|
||||
if (this.controlsHideTimer !== null) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
}
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
|
||||
this.setState({
|
||||
showDialpad: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onMicMuteClick = () => {
|
||||
const newVal = !this.state.micMuted;
|
||||
|
||||
|
@ -265,6 +293,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private closeDialpad = () => {
|
||||
this.setState({
|
||||
showDialpad: false,
|
||||
});
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
|
||||
private closeContextMenu = () => {
|
||||
this.setState({
|
||||
showMoreMenu: false,
|
||||
|
@ -306,37 +341,52 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onRoomAvatarClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onSecondaryRoomAvatarClick = () => {
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.secondaryCall.roomId,
|
||||
room_id: userFacingRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
private onCallResumeClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
|
||||
}
|
||||
|
||||
private onSecondaryCallResumeClick = () => {
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(this.props.secondaryCall.roomId);
|
||||
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoom = client.getRoom(this.props.call.roomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null;
|
||||
const callRoomId = CallHandler.roomIdForCall(this.props.call);
|
||||
const secondaryCallRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
|
||||
const callRoom = client.getRoom(callRoomId);
|
||||
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
|
||||
|
||||
let dialPad;
|
||||
let contextMenu;
|
||||
|
||||
if (this.state.showDialpad) {
|
||||
dialPad = <DialpadContextMenu
|
||||
{...alwaysAboveRightOf(
|
||||
this.dialpadButton.current.getBoundingClientRect(),
|
||||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
)}
|
||||
onFinished={this.closeDialpad}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (this.state.showMoreMenu) {
|
||||
contextMenu = <CallContextMenu
|
||||
{...aboveLeftOf(
|
||||
{...alwaysAboveLeftOf(
|
||||
this.contextMenuButton.current.getBoundingClientRect(),
|
||||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
|
@ -384,8 +434,15 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
onClick={this.onVidMuteClick}
|
||||
/> : null;
|
||||
|
||||
// The 'more' button actions are only relevant in a connected call
|
||||
// The dial pad & 'more' button actions are only relevant in a connected call
|
||||
// When not connected, we have to put something there to make the flexbox alignment correct
|
||||
const dialpadButton = this.state.callState === CallState.Connected ? <ContextMenuButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
|
||||
inputRef={this.dialpadButton}
|
||||
onClick={this.onDialpadClick}
|
||||
isExpanded={this.state.showDialpad}
|
||||
/> : <div className="mx_CallView_callControls_button mx_CallView_callControls_button_dialpad_hidden" />;
|
||||
|
||||
const contextMenuButton = this.state.callState === CallState.Connected ? <ContextMenuButton
|
||||
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
|
||||
onClick={this.onMoreClick}
|
||||
|
@ -396,7 +453,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
// in the near future, the dial pad button will go on the left. For now, it's the nothing button
|
||||
// because something needs to have margin-right: auto to make the alignment correct.
|
||||
const callControls = <div className={callControlsClasses}>
|
||||
<div className="mx_CallView_callControls_button mx_CallView_callControls_nothing" />
|
||||
{dialpadButton}
|
||||
<AccessibleButton
|
||||
className={micClasses}
|
||||
onClick={this.onMicMuteClick}
|
||||
|
@ -406,7 +463,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.props.call.roomId,
|
||||
room_id: callRoomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
@ -423,7 +480,9 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
|
||||
let onHoldText = null;
|
||||
if (this.state.isRemoteOnHold) {
|
||||
onHoldText = _t("You held the call <a>Resume</a>", {}, {
|
||||
const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
|
||||
_td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
|
||||
onHoldText = _t(holdString, {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
|
||||
{sub}
|
||||
</AccessibleButton>,
|
||||
|
@ -435,6 +494,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
let localVideoFeed = null;
|
||||
let onHoldContent = null;
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
|
@ -453,6 +513,9 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||
}
|
||||
if (!this.state.vidMuted) {
|
||||
localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />;
|
||||
}
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const maxVideoHeight = getFullScreenElement() ? null : (
|
||||
|
@ -468,7 +531,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize}
|
||||
maxHeight={maxVideoHeight}
|
||||
/>
|
||||
<VideoFeed type={VideoFeedType.Local} call={this.props.call} />
|
||||
{localVideoFeed}
|
||||
{onHoldContent}
|
||||
{callControls}
|
||||
</div>;
|
||||
|
@ -478,20 +541,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
mx_CallView_voice: true,
|
||||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
let secondaryCallAvatar: ReactNode;
|
||||
|
||||
if (this.props.secondaryCall) {
|
||||
const secAvatarSize = this.props.pipMode ? 40 : 100;
|
||||
secondaryCallAvatar = <div className="mx_CallView_voice_secondaryAvatarContainer"
|
||||
style={{width: secAvatarSize, height: secAvatarSize}}
|
||||
>
|
||||
<RoomAvatar
|
||||
room={secCallRoom}
|
||||
height={secAvatarSize}
|
||||
width={secAvatarSize}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
|
@ -502,7 +551,6 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
width={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
{secondaryCallAvatar}
|
||||
</div>
|
||||
<div className="mx_CallView_voice_holdText">{onHoldText}</div>
|
||||
{callControls}
|
||||
|
@ -546,7 +594,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
<AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
|
||||
<RoomAvatar room={secCallRoom} height={16} width={16} />
|
||||
<span className="mx_CallView_secondaryCall_roomName">
|
||||
{_t("%(name)s paused", { name: secCallRoom.name })}
|
||||
{_t("%(name)s on hold", { name: secCallRoom.name })}
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
|
@ -571,6 +619,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
return <div className={"mx_CallView " + myClassName}>
|
||||
{header}
|
||||
{contentView}
|
||||
{dialPad}
|
||||
{contextMenu}
|
||||
</div>;
|
||||
}
|
||||
|
|
85
src/components/views/voip/DialPad.tsx
Normal file
85
src/components/views/voip/DialPad.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
|
||||
|
||||
enum DialPadButtonKind {
|
||||
Digit,
|
||||
Delete,
|
||||
Dial,
|
||||
}
|
||||
|
||||
interface IButtonProps {
|
||||
kind: DialPadButtonKind;
|
||||
digit?: string;
|
||||
onButtonPress: (string) => void;
|
||||
}
|
||||
|
||||
class DialPadButton extends React.PureComponent<IButtonProps> {
|
||||
onClick = () => {
|
||||
this.props.onButtonPress(this.props.digit);
|
||||
}
|
||||
|
||||
render() {
|
||||
switch (this.props.kind) {
|
||||
case DialPadButtonKind.Digit:
|
||||
return <AccessibleButton className="mx_DialPad_button" onClick={this.onClick}>
|
||||
{this.props.digit}
|
||||
</AccessibleButton>;
|
||||
case DialPadButtonKind.Delete:
|
||||
return <AccessibleButton className="mx_DialPad_button mx_DialPad_deleteButton"
|
||||
onClick={this.onClick}
|
||||
/>;
|
||||
case DialPadButtonKind.Dial:
|
||||
return <AccessibleButton className="mx_DialPad_button mx_DialPad_dialButton" onClick={this.onClick} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onDigitPress: (string) => void;
|
||||
hasDialAndDelete: boolean;
|
||||
onDeletePress?: (string) => void;
|
||||
onDialPress?: (string) => void;
|
||||
}
|
||||
|
||||
export default class Dialpad extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
const buttonNodes = [];
|
||||
|
||||
for (const button of BUTTONS) {
|
||||
buttonNodes.push(<DialPadButton key={button} kind={DialPadButtonKind.Digit}
|
||||
digit={button} onButtonPress={this.props.onDigitPress}
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.hasDialAndDelete) {
|
||||
buttonNodes.push(<DialPadButton key="del" kind={DialPadButtonKind.Delete}
|
||||
onButtonPress={this.props.onDeletePress}
|
||||
/>);
|
||||
buttonNodes.push(<DialPadButton key="dial" kind={DialPadButtonKind.Dial}
|
||||
onButtonPress={this.props.onDialPress}
|
||||
/>);
|
||||
}
|
||||
|
||||
return <div className="mx_DialPad">
|
||||
{buttonNodes}
|
||||
</div>;
|
||||
}
|
||||
}
|
111
src/components/views/voip/DialPadModal.tsx
Normal file
111
src/components/views/voip/DialPadModal.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { ensureDMExists } from "../../../createRoom";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Field from "../elements/Field";
|
||||
import DialPad from './DialPad';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../../views/dialogs/ErrorDialog";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: '',
|
||||
}
|
||||
}
|
||||
|
||||
onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.setState({value: ev.target.value});
|
||||
}
|
||||
|
||||
onFormSubmit = (ev) => {
|
||||
ev.preventDefault();
|
||||
this.onDialPress();
|
||||
}
|
||||
|
||||
onDigitPress = (digit) => {
|
||||
this.setState({value: this.state.value + digit});
|
||||
}
|
||||
|
||||
onDeletePress = () => {
|
||||
if (this.state.value.length === 0) return;
|
||||
this.setState({value: this.state.value.slice(0, -1)});
|
||||
}
|
||||
|
||||
onDialPress = async () => {
|
||||
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
|
||||
if (!results || results.length === 0 || !results[0].userid) {
|
||||
Modal.createTrackedDialog('', '', ErrorDialog, {
|
||||
title: _t("Unable to look up phone number"),
|
||||
description: _t("There was an error looking up the phone number"),
|
||||
});
|
||||
}
|
||||
const userId = results[0].userid;
|
||||
|
||||
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="mx_DialPadModal">
|
||||
<div className="mx_DialPadModal_header">
|
||||
<div>
|
||||
<span className="mx_DialPadModal_title">{_t("Dial pad")}</span>
|
||||
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
|
||||
</div>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<Field className="mx_DialPadModal_field" id="dialpad_number"
|
||||
value={this.state.value} autoFocus={true}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div className="mx_DialPadModal_horizSep" />
|
||||
<div className="mx_DialPadModal_dialPad">
|
||||
<DialPad hasDialAndDelete={true}
|
||||
onDigitPress={this.onDigitPress}
|
||||
onDeletePress={this.onDeletePress}
|
||||
onDialPress={this.onDialPress}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -70,7 +70,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'answer',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -78,7 +78,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'reject',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -89,7 +89,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
|
||||
let room = null;
|
||||
if (this.state.incomingCall) {
|
||||
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
|
||||
room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall));
|
||||
}
|
||||
|
||||
const caller = room ? room.name : _t("Unknown caller");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue