Merge branch 'travis/moar-jitsi' into travis/addwidget-improvements

This commit is contained in:
Travis Ralston 2020-04-14 12:58:16 -06:00
commit 77bdde4021
33 changed files with 375 additions and 223 deletions

View file

@ -90,7 +90,6 @@
"qrcode-react": "^0.1.16", "qrcode-react": "^0.1.16",
"qs": "^6.6.0", "qs": "^6.6.0",
"react": "^16.9.0", "react": "^16.9.0",
"react-addons-css-transition-group": "15.6.2",
"react-beautiful-dnd": "^4.0.1", "react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-focus-lock": "^2.2.1", "react-focus-lock": "^2.2.1",

View file

@ -94,6 +94,7 @@
@import "./views/elements/_AccessibleButton.scss"; @import "./views/elements/_AccessibleButton.scss";
@import "./views/elements/_AddressSelector.scss"; @import "./views/elements/_AddressSelector.scss";
@import "./views/elements/_AddressTile.scss"; @import "./views/elements/_AddressTile.scss";
@import "./views/elements/_ButtonPlaceholder.scss";
@import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_DirectorySearchBox.scss";
@import "./views/elements/_Dropdown.scss"; @import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@ -133,6 +134,7 @@
@import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MNoticeBody.scss";
@import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss";
@import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_MjolnirBody.scss"; @import "./views/messages/_MjolnirBody.scss";

View file

@ -44,6 +44,7 @@ limitations under the License.
.mx_CompleteSecurity_actionRow { .mx_CompleteSecurity_actionRow {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: $font-28px;
.mx_AccessibleButton { .mx_AccessibleButton {
margin-inline-start: 18px; margin-inline-start: 18px;

View file

@ -0,0 +1,24 @@
/*
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.
*/
.mx_ButtonPlaceholder {
font-size: $font-14px;
font-weight: 600;
padding: 7px 18px;
display: inline-block;
text-align: center;
color: $authpage-secondary-color;
}

View file

@ -0,0 +1,22 @@
/*
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.
*/
span.mx_MVideoBody {
video.mx_MVideoBody {
max-width: 100%;
height: auto;
}
}

View file

@ -34,12 +34,17 @@ limitations under the License.
background-color: $reaction-row-button-selected-bg-color; background-color: $reaction-row-button-selected-bg-color;
border-color: $reaction-row-button-selected-border-color; border-color: $reaction-row-button-selected-border-color;
} }
}
.mx_ReactionsRowButton_content { // ignore mouse events for all children, treat it as one entire hoverable entity
max-width: 100px; * {
overflow: hidden; pointer-events: none;
white-space: nowrap; }
text-overflow: ellipsis;
padding-right: 4px; .mx_ReactionsRowButton_content {
max-width: 100px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-right: 4px;
}
} }

View file

@ -44,27 +44,29 @@ limitations under the License.
outline: none; outline: none;
overflow-x: hidden; overflow-x: hidden;
span.mx_UserPill, span.mx_RoomPill { &.mx_BasicMessageComposer_input_shouldShowPillAvatar {
padding-left: 21px; span.mx_UserPill, span.mx_RoomPill {
position: relative; padding-left: 21px;
position: relative;
// avatar psuedo element // avatar psuedo element
&::before { &::before {
position: absolute; position: absolute;
left: 2px; left: 2px;
top: 2px; top: 2px;
content: var(--avatar-letter); content: var(--avatar-letter);
width: 16px; width: 16px;
height: 16px; height: 16px;
background: var(--avatar-background), $avatar-bg-color; background: var(--avatar-background), $avatar-bg-color;
color: $avatar-initial-color; color: $avatar-initial-color;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 16px; background-size: 16px;
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
font-weight: normal; font-weight: normal;
line-height: $font-16px; line-height: $font-16px;
font-size: $font-10-4px; font-size: $font-10-4px;
}
} }
} }
} }

View file

@ -188,4 +188,8 @@ export default class BasePlatform {
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl()); const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl());
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
} }
onKeyDown(ev: KeyboardEvent): boolean {
return false; // no shortcuts implemented
}
} }

View file

@ -185,7 +185,7 @@ export async function promptForBackupPassphrase() {
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
showSummary: false, keyCallback: k => key = k, showSummary: false, keyCallback: k => key = k,
}, null, /* priority = */ false, /* static = */ true); }, null, /* priority = */ false, /* static = */ true);
const success = await finished; const success = await finished;

View file

@ -43,6 +43,8 @@ export const Key = {
BACKTICK: "`", BACKTICK: "`",
SPACE: " ", SPACE: " ",
SLASH: "/", SLASH: "/",
SQUARE_BRACKET_LEFT: "[",
SQUARE_BRACKET_RIGHT: "]",
A: "a", A: "a",
B: "b", B: "b",
C: "c", C: "c",

View file

@ -172,6 +172,7 @@ Request:
Response: Response:
[ [
{ {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
type: "im.vector.modular.widgets", type: "im.vector.modular.widgets",
state_key: "wid1", state_key: "wid1",
content: { content: {
@ -190,6 +191,7 @@ Example:
room_id: "!foo:bar", room_id: "!foo:bar",
response: [ response: [
{ {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
type: "im.vector.modular.widgets", type: "im.vector.modular.widgets",
state_key: "wid1", state_key: "wid1",
content: { content: {

View file

@ -353,7 +353,7 @@ export const Commands = [
return success(cli.setRoomTopic(roomId, args)); return success(cli.setRoomTopic(roomId, args));
} }
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId); if (!room) return reject(_t("Failed to set topic"));
const topicEvents = room.currentState.getStateEvents('m.room.topic', ''); const topicEvents = room.currentState.getStateEvents('m.room.topic', '');
const topic = topicEvents && topicEvents.getContent().topic; const topic = topicEvents && topicEvents.getContent().topic;
@ -724,9 +724,10 @@ export const Commands = [
if (!isNaN(powerLevel)) { if (!isNaN(powerLevel)) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId); if (!room) return reject(_t("Command failed"));
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room"));
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
} }
} }
@ -745,9 +746,10 @@ export const Commands = [
if (matches) { if (matches) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId); if (!room) return reject(_t("Command failed"));
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room"));
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent)); return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
} }
} }
@ -949,7 +951,7 @@ export const Commands = [
// Command definitions for autocompletion ONLY: // Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
new Command({ new Command({
command: 'me', command: "me",
args: '<message>', args: '<message>',
description: _td('Displays action'), description: _td('Displays action'),
category: CommandCategories.messages, category: CommandCategories.messages,
@ -966,16 +968,7 @@ Commands.forEach(cmd => {
}); });
}); });
export function parseCommandString(input) {
/**
* Process the given text for /commands and return a bound method to perform them.
* @param {string} roomId The room in which the command was performed.
* @param {string} input The raw text input by the user.
* @return {null|function(): Object} Function returning an object with the property 'error' if there was an error
* processing the command, or 'promise' if a request was sent out.
* Returns null if the input didn't match a command.
*/
export function getCommand(roomId, input) {
// trim any trailing whitespace, as it can confuse the parser for // trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands // IRC-style commands
input = input.replace(/\s+$/, ''); input = input.replace(/\s+$/, '');
@ -991,6 +984,20 @@ export function getCommand(roomId, input) {
cmd = input; cmd = input;
} }
return {cmd, args};
}
/**
* Process the given text for /commands and return a bound method to perform them.
* @param {string} roomId The room in which the command was performed.
* @param {string} input The raw text input by the user.
* @return {null|function(): Object} Function returning an object with the property 'error' if there was an error
* processing the command, or 'promise' if a request was sent out.
* Returns null if the input didn't match a command.
*/
export function getCommand(roomId, input) {
const {cmd, args} = parseCommandString(input);
if (CommandMap.has(cmd)) { if (CommandMap.has(cmd)) {
return () => CommandMap.get(cmd).run(roomId, args, cmd); return () => CommandMap.get(cmd).run(roomId, args, cmd);
} }

View file

@ -603,6 +603,7 @@ const stateHandlers = {
'm.room.guest_access': textForGuestAccessEvent, 'm.room.guest_access': textForGuestAccessEvent,
'm.room.related_groups': textForRelatedGroupsEvent, 'm.room.related_groups': textForRelatedGroupsEvent,
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
'im.vector.modular.widgets': textForWidgetEvent, 'im.vector.modular.widgets': textForWidgetEvent,
}; };

View file

@ -70,6 +70,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._recoveryKey = null; this._recoveryKey = null;
this._recoveryKeyNode = null; this._recoveryKeyNode = null;
this._setZxcvbnResultTimeout = null; this._setZxcvbnResultTimeout = null;
this._backupKey = null;
this.state = { this.state = {
phase: PHASE_LOADING, phase: PHASE_LOADING,
@ -243,7 +244,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
createSecretStorageKey: async () => this._recoveryKey, createSecretStorageKey: async () => this._recoveryKey,
keyBackupInfo: this.state.backupInfo, keyBackupInfo: this.state.backupInfo,
setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup,
getKeyBackupPassphrase: promptForBackupPassphrase, getKeyBackupPassphrase: () => {
// We may already have the backup key if we earlier went
// through the restore backup path, so pass it along
// rather than prompting again.
if (this._backupKey) {
return this._backupKey;
}
return promptForBackupPassphrase();
},
}); });
} }
this.setState({ this.setState({
@ -272,10 +281,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
_restoreBackup = async () => { _restoreBackup = async () => {
// It's possible we'll need the backup key later on for bootstrapping,
// so let's stash it here, rather than prompting for it twice.
const keyCallback = k => this._backupKey = k;
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog( const { finished } = Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null, 'Restore Backup', '', RestoreKeyBackupDialog,
/* priority = */ false, /* static = */ false, {
showSummary: false,
keyCallback,
},
null, /* priority = */ false, /* static = */ false,
); );
await finished; await finished;

View file

@ -100,6 +100,8 @@ export default class EmojiProvider extends AutocompleteProvider {
// then sort by score (Infinity if matchedString not in shortname) // then sort by score (Infinity if matchedString not in shortname)
sorters.push((c) => score(matchedString, c.shortname)); sorters.push((c) => score(matchedString, c.shortname));
// then sort by max score of all shortcodes, trim off the `:`
sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s))));
// If the matchedString is not empty, sort by length of shortname. Example: // If the matchedString is not empty, sort by length of shortname. Example:
// matchedString = ":bookmark" // matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...] // completions = [":bookmark:", ":bookmark_tabs:", ...]

View file

@ -1,7 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd Copyright 2017, 2018, 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,10 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient } from 'matrix-js-sdk'; import * as React from 'react';
import React, {createRef} from 'react'; import * as PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import PropTypes from 'prop-types'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard'; import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
@ -29,7 +29,7 @@ import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore'; import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore"; import RoomListStore from "../../stores/RoomListStore";
@ -40,6 +40,8 @@ import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage"; import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg";
// We need to fetch each pinned message individually (if we don't already have it) // 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. // so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general. // NB. this is just for server notices rather than pinned messages in general.
@ -52,6 +54,52 @@ function canElementReceiveInput(el) {
!!el.getAttribute("contenteditable"); !!el.getAttribute("contenteditable");
} }
interface IProps {
matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
middleDisabled: boolean;
initialEventPixelOffset: number;
leftDisabled: boolean;
rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string;
autoJoin: boolean;
thirdPartyInvite?: object;
roomOobData?: object;
currentRoomId: string;
ConferenceHandler?: object;
collapseLhs: boolean;
checkingForUpdate: boolean;
config: {
piwik: {
policyUrl: string;
},
[key: string]: any,
};
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
}
interface IState {
mouseDown?: {
x: number;
y: number;
};
syncErrorData: any;
useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
}
/** /**
* This is what our MatrixChat shows when we are logged in. The precise view is * This is what our MatrixChat shows when we are logged in. The precise view is
* determined by the page_type property. * determined by the page_type property.
@ -61,10 +109,10 @@ function canElementReceiveInput(el) {
* *
* Components mounted below us can access the matrix client via the react context. * Components mounted below us can access the matrix client via the react context.
*/ */
const LoggedInView = createReactClass({ class LoggedInView extends React.PureComponent<IProps, IState> {
displayName: 'LoggedInView', static displayName = 'LoggedInView';
propTypes: { static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
page_type: PropTypes.string.isRequired, page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func, onRoomCreated: PropTypes.func,
@ -77,25 +125,28 @@ const LoggedInView = createReactClass({
viaServers: PropTypes.arrayOf(PropTypes.string), viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff. // and lots and lots of other stuff.
}, };
getInitialState: function() { protected readonly _matrixClient: MatrixClient;
return { protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
protected readonly _sessionStore: sessionStore;
protected readonly _sessionStoreToken: { remove: () => void };
protected resizer: Resizer;
constructor(props, context) {
super(props, context);
this.state = {
mouseDown: undefined,
syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view // use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events // any currently active server notice events
serverNoticeEvents: [], serverNoticeEvents: [],
}; };
},
componentDidMount: function() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted // stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient; this._matrixClient = this.props.matrixClient;
@ -117,22 +168,29 @@ const LoggedInView = createReactClass({
fixupColorFonts(); fixupColorFonts();
this._roomView = createRef(); this._roomView = React.createRef();
}, this._resizeContainer = React.createRef();
}
componentDidUpdate(prevProps) { componentDidMount() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
}
componentDidUpdate(prevProps, prevState) {
// attempt to guess when a banner was opened or closed // attempt to guess when a banner was opened or closed
if ( if (
(prevProps.showCookieBar !== this.props.showCookieBar) || (prevProps.showCookieBar !== this.props.showCookieBar) ||
(prevProps.hasNewVersion !== this.props.hasNewVersion) || (prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) || (prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar) (prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
) { ) {
this.props.resizeNotifier.notifyBannersChanged(); this.props.resizeNotifier.notifyBannersChanged();
} }
}, }
componentWillUnmount: function() { componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false); document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("sync", this.onSync);
@ -141,7 +199,7 @@ const LoggedInView = createReactClass({
this._sessionStoreToken.remove(); this._sessionStoreToken.remove();
} }
this.resizer.detach(); this.resizer.detach();
}, }
// Child components assume that the client peg will not be null, so give them some // Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy. // sort of assurance here by only allowing a re-render if the client is truthy.
@ -149,22 +207,22 @@ const LoggedInView = createReactClass({
// This is required because `LoggedInView` maintains its own state and if this state // This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will // updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors. // attempt to re-render and the children will throw errors.
shouldComponentUpdate: function() { shouldComponentUpdate() {
return Boolean(MatrixClientPeg.get()); return Boolean(MatrixClientPeg.get());
}, }
canResetTimelineInRoom: function(roomId) { canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) { if (!this._roomView.current) {
return true; return true;
} }
return this._roomView.current.canResetTimeline(); return this._roomView.current.canResetTimeline();
}, };
_setStateFromSessionStore() { _setStateFromSessionStore = () => {
this.setState({ this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
}); });
}, };
_createResizer() { _createResizer() {
const classNames = { const classNames = {
@ -188,24 +246,22 @@ const LoggedInView = createReactClass({
}, },
}; };
const resizer = new Resizer( const resizer = new Resizer(
this.resizeContainer, this._resizeContainer.current,
CollapseDistributor, CollapseDistributor,
collapseConfig); collapseConfig);
resizer.setClassNames(classNames); resizer.setClassNames(classNames);
return resizer; return resizer;
}, }
_loadResizerPreferences() { _loadResizerPreferences() {
let lhsSize = window.localStorage.getItem("mx_lhs_size"); let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
if (lhsSize !== null) { if (isNaN(lhsSize)) {
lhsSize = parseInt(lhsSize, 10);
} else {
lhsSize = 350; lhsSize = 350;
} }
this.resizer.forHandleAt(0).resize(lhsSize); this.resizer.forHandleAt(0).resize(lhsSize);
}, }
onAccountData: function(event) { onAccountData = (event) => {
if (event.getType() === "im.vector.web.settings") { if (event.getType() === "im.vector.web.settings") {
this.setState({ this.setState({
useCompactLayout: event.getContent().useCompactLayout, useCompactLayout: event.getContent().useCompactLayout,
@ -214,9 +270,9 @@ const LoggedInView = createReactClass({
if (event.getType() === "m.ignored_user_list") { if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"}); dis.dispatch({action: "ignore_state_changed"});
} }
}, };
onSync: function(syncState, oldSyncState, data) { onSync = (syncState, oldSyncState, data) => {
const oldErrCode = ( const oldErrCode = (
this.state.syncErrorData && this.state.syncErrorData &&
this.state.syncErrorData.error && this.state.syncErrorData.error &&
@ -238,16 +294,16 @@ const LoggedInView = createReactClass({
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}, };
onRoomStateEvents: function(ev, state) { onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStore.getRoomLists(); const roomLists = RoomListStore.getRoomLists();
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}, };
_updateServerNoticeEvents: async function() { _updateServerNoticeEvents = async () => {
const roomLists = RoomListStore.getRoomLists(); const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return []; if (!roomLists['m.server_notice']) return [];
@ -260,16 +316,16 @@ const LoggedInView = createReactClass({
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) { for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const ev = timeline.getEvents().find(ev => ev.getId() === eventId); const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (ev) pinnedEvents.push(ev); if (event) pinnedEvents.push(event);
} }
} }
this.setState({ this.setState({
serverNoticeEvents: pinnedEvents, serverNoticeEvents: pinnedEvents,
}); });
}, };
_onPaste: function(ev) { _onPaste = (ev) => {
let canReceiveInput = false; let canReceiveInput = false;
let element = ev.target; let element = ev.target;
// test for all parents because the target can be a child of a contenteditable element // test for all parents because the target can be a child of a contenteditable element
@ -283,7 +339,7 @@ const LoggedInView = createReactClass({
// so dispatch synchronously before paste happens // so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true); dis.dispatch({action: 'focus_composer'}, true);
} }
}, };
/* /*
SOME HACKERY BELOW: SOME HACKERY BELOW:
@ -307,22 +363,22 @@ const LoggedInView = createReactClass({
We also listen with a native listener on the document to get keydown events when no element is focused. We also listen with a native listener on the document to get keydown events when no element is focused.
Bubbling is irrelevant here as the target is the body element. Bubbling is irrelevant here as the target is the body element.
*/ */
_onReactKeyDown: function(ev) { _onReactKeyDown = (ev) => {
// events caught while bubbling up on the root element // events caught while bubbling up on the root element
// of this component, so something must be focused. // of this component, so something must be focused.
this._onKeyDown(ev); this._onKeyDown(ev);
}, };
_onNativeKeyDown: function(ev) { _onNativeKeyDown = (ev) => {
// only pass this if there is no focused element. // only pass this if there is no focused element.
// if there is, _onKeyDown will be called by the // if there is, _onKeyDown will be called by the
// react keydown handler that respects the react bubbling order. // react keydown handler that respects the react bubbling order.
if (ev.target === document.body) { if (ev.target === document.body) {
this._onKeyDown(ev); this._onKeyDown(ev);
} }
}, };
_onKeyDown: function(ev) { _onKeyDown = (ev) => {
/* /*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
// Will need to find a better meta key if anyone actually cares about using this. // Will need to find a better meta key if anyone actually cares about using this.
@ -407,6 +463,11 @@ const LoggedInView = createReactClass({
}); });
handled = true; handled = true;
} }
break;
default:
// if we do not have a handler for it, pass it to the platform which might
handled = PlatformPeg.get().onKeyDown(ev);
} }
if (handled) { if (handled) {
@ -432,19 +493,19 @@ const LoggedInView = createReactClass({
// that would prevent typing in the now-focussed composer // that would prevent typing in the now-focussed composer
} }
} }
}, };
/** /**
* dispatch a page-up/page-down/etc to the appropriate component * dispatch a page-up/page-down/etc to the appropriate component
* @param {Object} ev The key event * @param {Object} ev The key event
*/ */
_onScrollKeyPressed: function(ev) { _onScrollKeyPressed = (ev) => {
if (this._roomView.current) { if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev); this._roomView.current.handleScrollKey(ev);
} }
}, };
_onDragEnd: function(result) { _onDragEnd = (result) => {
// Dragged to an invalid destination, not onto a droppable // Dragged to an invalid destination, not onto a droppable
if (!result.destination) { if (!result.destination) {
return; return;
@ -467,9 +528,9 @@ const LoggedInView = createReactClass({
} else if (dest.startsWith('room-sub-list-droppable_')) { } else if (dest.startsWith('room-sub-list-droppable_')) {
this._onRoomTileEndDrag(result); this._onRoomTileEndDrag(result);
} }
}, };
_onRoomTileEndDrag: function(result) { _onRoomTileEndDrag = (result) => {
let newTag = result.destination.droppableId.split('_')[1]; let newTag = result.destination.droppableId.split('_')[1];
let prevTag = result.source.droppableId.split('_')[1]; let prevTag = result.source.droppableId.split('_')[1];
if (newTag === 'undefined') newTag = undefined; if (newTag === 'undefined') newTag = undefined;
@ -486,9 +547,9 @@ const LoggedInView = createReactClass({
prevTag, newTag, prevTag, newTag,
oldIndex, newIndex, oldIndex, newIndex,
), true); ), true);
}, };
_onMouseDown: function(ev) { _onMouseDown = (ev) => {
// When the panels are disabled, clicking on them results in a mouse event // When the panels are disabled, clicking on them results in a mouse event
// which bubbles to certain elements in the tree. When this happens, close // which bubbles to certain elements in the tree. When this happens, close
// any settings page that is currently open (user/room/group). // any settings page that is currently open (user/room/group).
@ -507,9 +568,9 @@ const LoggedInView = createReactClass({
}); });
} }
} }
}, };
_onMouseUp: function(ev) { _onMouseUp = (ev) => {
if (!this.state.mouseDown) return; if (!this.state.mouseDown) return;
const deltaX = ev.pageX - this.state.mouseDown.x; const deltaX = ev.pageX - this.state.mouseDown.x;
@ -528,13 +589,9 @@ const LoggedInView = createReactClass({
// Always clear the mouseDown state to ensure we don't accidentally // Always clear the mouseDown state to ensure we don't accidentally
// use stale values due to the mouseDown checks. // use stale values due to the mouseDown checks.
this.setState({mouseDown: null}); this.setState({mouseDown: null});
}, };
_setResizeContainerRef(div) { render() {
this.resizeContainer = div;
},
render: function() {
const LeftPanel = sdk.getComponent('structures.LeftPanel'); const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView'); const UserView = sdk.getComponent('structures.UserView');
@ -647,7 +704,7 @@ const LoggedInView = createReactClass({
{ topBar } { topBar }
<ToastContainer /> <ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}> <DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}> <div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel <LeftPanel
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false} collapsed={this.props.collapseLhs || false}
@ -660,7 +717,7 @@ const LoggedInView = createReactClass({
</div> </div>
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );
}, }
}); }
export default LoggedInView; export default LoggedInView;

View file

@ -49,7 +49,6 @@ import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import WidgetUtils from '../../utils/WidgetUtils';
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
@ -406,13 +405,9 @@ export default createReactClass({
const hideWidgetDrawer = localStorage.getItem( const hideWidgetDrawer = localStorage.getItem(
room.roomId + "_hide_widget_drawer"); room.roomId + "_hide_widget_drawer");
if (hideWidgetDrawer === "true") { // This is confusing, but it means to say that we default to the tray being
return false; // hidden unless the user clicked to open it.
} return hideWidgetDrawer === "false";
const widgets = WidgetEchoStore.getEchoedRoomWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
return widgets.length > 0 || WidgetEchoStore.roomHasPendingWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
}, },
componentDidMount: function() { componentDidMount: function() {

View file

@ -108,14 +108,13 @@ export default class SetupEncryptionBody extends React.Component {
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>; />;
} else if (phase === PHASE_INTRO) { } else if (phase === PHASE_INTRO) {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); const ButtonPlaceholder = sdk.getComponent("elements.ButtonPlaceholder");
return ( return (
<div> <div>
<p>{_t( <p>{_t(
"Open an existing session & use it to verify this one, " + "Use an existing session to verify this one, " +
"granting it access to encrypted messages.", "granting it access to encrypted messages.",
)}</p> )}</p>
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
<p>{_t( <p>{_t(
"If you cant access one, <button>use your recovery key or passphrase.</button>", "If you cant access one, <button>use your recovery key or passphrase.</button>",
{}, { {}, {
@ -133,6 +132,7 @@ export default class SetupEncryptionBody extends React.Component {
> >
{_t("Skip")} {_t("Skip")}
</AccessibleButton> </AccessibleButton>
<ButtonPlaceholder>{_t("Use your other device to continue…")}</ButtonPlaceholder>
</div> </div>
</div> </div>
); );

View file

@ -66,7 +66,7 @@ export default createReactClass({
} }
if (!this.state.isPublic && SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (!this.state.isPublic && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
createOpts.encryption = this.state.isEncrypted; opts.encryption = this.state.isEncrypted;
} }
return opts; return opts;

View file

@ -0,0 +1,19 @@
/*
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.
*/
export default function ButtonPlaceholder(props) {
return <div class="mx_ButtonPlaceholder">{props.children}</div>;
}

View file

@ -81,12 +81,14 @@ export default createReactClass({
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer'; const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) { switch (action.action) {
case 'appsDrawer': case 'appsDrawer':
// Note: these booleans are awkward because localstorage is fundamentally
// string-based. We also do exact equality on the strings later on.
if (action.show) { if (action.show) {
localStorage.removeItem(hideWidgetKey); localStorage.setItem(hideWidgetKey, "false");
} else { } else {
// Store hidden state of widget // Store hidden state of widget
// Don't show if previously hidden // Don't show if previously hidden
localStorage.setItem(hideWidgetKey, true); localStorage.setItem(hideWidgetKey, "true");
} }
break; break;

View file

@ -39,6 +39,7 @@ import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {EMOTICON_TO_EMOJI} from "../../../emoji"; import {EMOTICON_TO_EMOJI} from "../../../emoji";
import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands";
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
@ -84,6 +85,7 @@ export default class BasicMessageEditor extends React.Component {
super(props); super(props);
this.state = { this.state = {
autoComplete: null, autoComplete: null,
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
}; };
this._editorRef = null; this._editorRef = null;
this._autocompleteRef = null; this._autocompleteRef = null;
@ -92,6 +94,7 @@ export default class BasicMessageEditor extends React.Component {
this._isIMEComposing = false; this._isIMEComposing = false;
this._hasTextSelected = false; this._hasTextSelected = false;
this._emoticonSettingHandle = null; this._emoticonSettingHandle = null;
this._shouldShowPillAvatarSettingHandle = null;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -162,7 +165,16 @@ export default class BasicMessageEditor extends React.Component {
} }
this.setState({autoComplete: this.props.model.autoComplete}); this.setState({autoComplete: this.props.model.autoComplete});
this.historyManager.tryPush(this.props.model, selection, inputType, diff); this.historyManager.tryPush(this.props.model, selection, inputType, diff);
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty);
let isTyping = !this.props.model.isEmpty;
// If the user is entering a command, only consider them typing if it is one which sends a message into the room
if (isTyping && this.props.model.parts[0].type === "command") {
const {cmd} = parseCommandString(this.props.model.parts[0].text);
if (!CommandMap.has(cmd) || CommandMap.get(cmd).category !== CommandCategories.messages) {
isTyping = false;
}
}
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, isTyping);
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(); this.props.onChange();
@ -508,10 +520,15 @@ export default class BasicMessageEditor extends React.Component {
this.setState({completionIndex}); this.setState({completionIndex});
} }
_configureEmoticonAutoReplace() { _configureEmoticonAutoReplace = () => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji'); const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
this.props.model.setTransformCallback(shouldReplace ? this._replaceEmoticon : null); this.props.model.setTransformCallback(shouldReplace ? this._replaceEmoticon : null);
} };
_configureShouldShowPillAvatar = () => {
const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
this.setState({ showPillAvatar });
};
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener("selectionchange", this._onSelectionChange); document.removeEventListener("selectionchange", this._onSelectionChange);
@ -519,15 +536,17 @@ export default class BasicMessageEditor extends React.Component {
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true); this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true); this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
SettingsStore.unwatchSetting(this._emoticonSettingHandle); SettingsStore.unwatchSetting(this._emoticonSettingHandle);
SettingsStore.unwatchSetting(this._shouldShowPillAvatarSettingHandle);
} }
componentDidMount() { componentDidMount() {
const model = this.props.model; const model = this.props.model;
model.setUpdateCallback(this._updateEditorState); model.setUpdateCallback(this._updateEditorState);
this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, () => { this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
this._configureEmoticonAutoReplace(); this._configureEmoticonAutoReplace);
});
this._configureEmoticonAutoReplace(); this._configureEmoticonAutoReplace();
this._shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
this._configureShouldShowPillAvatar);
const partCreator = model.partCreator; const partCreator = model.partCreator;
// TODO: does this allow us to get rid of EditorStateTransfer? // TODO: does this allow us to get rid of EditorStateTransfer?
// not really, but we could not serialize the parts, and just change the autoCompleter // not really, but we could not serialize the parts, and just change the autoCompleter
@ -605,9 +624,12 @@ export default class BasicMessageEditor extends React.Component {
/> />
</div>); </div>);
} }
const classes = classNames("mx_BasicMessageComposer", { const wrapperClasses = classNames("mx_BasicMessageComposer", {
"mx_BasicMessageComposer_input_error": this.state.showVisualBell, "mx_BasicMessageComposer_input_error": this.state.showVisualBell,
}); });
const classes = classNames("mx_BasicMessageComposer_input", {
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
});
const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar'); const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar');
const shortcuts = { const shortcuts = {
@ -618,11 +640,11 @@ export default class BasicMessageEditor extends React.Component {
const {completionIndex} = this.state; const {completionIndex} = this.state;
return (<div className={classes}> return (<div className={wrapperClasses}>
{ autoComplete } { autoComplete }
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} /> <MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
<div <div
className="mx_BasicMessageComposer_input" className={classes}
contentEditable="true" contentEditable="true"
tabIndex="0" tabIndex="0"
onBlur={this._onBlur} onBlur={this._onBlur}

View file

@ -59,6 +59,7 @@ const stateEventTileTypes = {
'm.room.power_levels': 'messages.TextualEvent', 'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent', 'm.room.pinned_events': 'messages.TextualEvent',
'm.room.server_acl': 'messages.TextualEvent', 'm.room.server_acl': 'messages.TextualEvent',
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
'im.vector.modular.widgets': 'messages.TextualEvent', 'im.vector.modular.widgets': 'messages.TextualEvent',
'm.room.tombstone': 'messages.TextualEvent', 'm.room.tombstone': 'messages.TextualEvent',
'm.room.join_rules': 'messages.TextualEvent', 'm.room.join_rules': 'messages.TextualEvent',

View file

@ -139,7 +139,7 @@ export default class DevicesPanel extends React.Component {
body: _t("Click the button below to confirm deleting these sessions.", { body: _t("Click the button below to confirm deleting these sessions.", {
count: numDevices, count: numDevices,
}), }),
continueText: _t("Delete sessions"), continueText: _t("Delete sessions", {count: numDevices}),
continueKind: "danger", continueKind: "danger",
}, },
}; };

View file

@ -33,6 +33,7 @@ const plEventsToLabels = {
"m.room.tombstone": _td("Upgrade the room"), "m.room.tombstone": _td("Upgrade the room"),
"m.room.encryption": _td("Enable room encryption"), "m.room.encryption": _td("Enable room encryption"),
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
"im.vector.modular.widgets": _td("Modify widgets"), "im.vector.modular.widgets": _td("Modify widgets"),
}; };
@ -47,6 +48,7 @@ const plEventsToShow = {
"m.room.tombstone": {isState: true}, "m.room.tombstone": {isState: true},
"m.room.encryption": {isState: true}, "m.room.encryption": {isState: true},
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
"im.vector.modular.widgets": {isState: true}, "im.vector.modular.widgets": {isState: true},
}; };

View file

@ -20,8 +20,8 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import VerificationQRCode from "../elements/crypto/VerificationQRCode"; import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
@replaceableComponent("views.verification.VerificationQREmojiOptions") @replaceableComponent("views.verification.VerificationQREmojiOptions")
export default class VerificationQREmojiOptions extends React.Component { export default class VerificationQREmojiOptions extends React.Component {
@ -31,31 +31,17 @@ export default class VerificationQREmojiOptions extends React.Component {
onStartEmoji: PropTypes.func.isRequired, onStartEmoji: PropTypes.func.isRequired,
}; };
constructor(props) {
super(props);
this.state = {
qrProps: null,
};
this._prepareQrCode(props.request);
}
async _prepareQrCode(request: VerificationRequest) {
try {
const props = await VerificationQRCode.getPropsForRequest(request);
this.setState({qrProps: props});
} catch (e) {
console.error(e);
// We just won't show a QR code
}
}
render() { render() {
let qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>; const {request} = this.props;
if (this.state.qrProps) { const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
qrCode = <VerificationQRCode {...this.state.qrProps} />;
let qrCode;
if (showQR) {
qrCode = <VerificationQRCode qrCodeData={request.qrCodeData} />;
} else {
qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
} }
return ( return (
<div> <div>
{_t("Verify this session by completing one of the following:")} {_t("Verify this session by completing one of the following:")}

View file

@ -132,12 +132,12 @@ export default class VerificationShowSas extends React.Component {
/>; />;
} else { } else {
confirm = <React.Fragment> confirm = <React.Fragment>
<AccessibleButton onClick={this.onMatchClick} kind="primary">
{ _t("They match") }
</AccessibleButton>
<AccessibleButton onClick={this.onDontMatchClick} kind="danger"> <AccessibleButton onClick={this.onDontMatchClick} kind="danger">
{ _t("They don't match") } { _t("They don't match") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton onClick={this.onMatchClick} kind="primary">
{ _t("They match") }
</AccessibleButton>
</React.Fragment>; </React.Fragment>;
} }

View file

@ -177,6 +177,7 @@
"Changes your avatar in this current room only": "Changes your avatar in this current room only", "Changes your avatar in this current room only": "Changes your avatar in this current room only",
"Changes your avatar in all rooms": "Changes your avatar in all rooms", "Changes your avatar in all rooms": "Changes your avatar in all rooms",
"Gets or sets the room topic": "Gets or sets the room topic", "Gets or sets the room topic": "Gets or sets the room topic",
"Failed to set topic": "Failed to set topic",
"This room has no topic.": "This room has no topic.", "This room has no topic.": "This room has no topic.",
"Sets the room name": "Sets the room name", "Sets the room name": "Sets the room name",
"Invites user with given id to current room": "Invites user with given id to current room", "Invites user with given id to current room": "Invites user with given id to current room",
@ -196,6 +197,8 @@
"Unignored user": "Unignored user", "Unignored user": "Unignored user",
"You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s", "You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s",
"Define the power level of a user": "Define the power level of a user", "Define the power level of a user": "Define the power level of a user",
"Command failed": "Command failed",
"Could not find user in room": "Could not find user in room",
"Deops user with given id": "Deops user with given id", "Deops user with given id": "Deops user with given id",
"Opens the Developer Tools dialog": "Opens the Developer Tools dialog", "Opens the Developer Tools dialog": "Opens the Developer Tools dialog",
"Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room", "Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room",
@ -619,7 +622,8 @@
"Confirm deleting these sessions": "Confirm deleting these sessions", "Confirm deleting these sessions": "Confirm deleting these sessions",
"Click the button below to confirm deleting these sessions.|other": "Click the button below to confirm deleting these sessions.", "Click the button below to confirm deleting these sessions.|other": "Click the button below to confirm deleting these sessions.",
"Click the button below to confirm deleting these sessions.|one": "Click the button below to confirm deleting this session.", "Click the button below to confirm deleting these sessions.|one": "Click the button below to confirm deleting this session.",
"Delete sessions": "Delete sessions", "Delete sessions|other": "Delete sessions",
"Delete sessions|one": "Delete session",
"Authentication": "Authentication", "Authentication": "Authentication",
"Delete %(count)s sessions|other": "Delete %(count)s sessions", "Delete %(count)s sessions|other": "Delete %(count)s sessions",
"Delete %(count)s sessions|one": "Delete %(count)s session", "Delete %(count)s sessions|one": "Delete %(count)s session",
@ -2112,9 +2116,9 @@
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.", "You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
"Registration Successful": "Registration Successful", "Registration Successful": "Registration Successful",
"Create your account": "Create your account", "Create your account": "Create your account",
"Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.", "Use an existing session to verify this one, granting it access to encrypted messages.": "Use an existing session to verify this one, granting it access to encrypted messages.",
"Waiting…": "Waiting…",
"If you cant access one, <button>use your recovery key or passphrase.</button>": "If you cant access one, <button>use your recovery key or passphrase.</button>", "If you cant access one, <button>use your recovery key or passphrase.</button>": "If you cant access one, <button>use your recovery key or passphrase.</button>",
"Use your other device to continue…": "Use your other device to continue…",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.", "Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.",

View file

@ -64,6 +64,7 @@ class ActiveWidgetStore extends EventEmitter {
// Everything else relies on views listening for events and calling setters // Everything else relies on views listening for events and calling setters
// on this class which is terrible. This store should just listen for events // on this class which is terrible. This store should just listen for events
// and keep itself up to date. // and keep itself up to date.
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
if (ev.getType() !== 'im.vector.modular.widgets') return; if (ev.getType() !== 'im.vector.modular.widgets') return;
if (ev.getStateKey() === this._persistentWidgetId) { if (ev.getStateKey() === this._persistentWidgetId) {

View file

@ -68,6 +68,7 @@ export default class WidgetUtils {
return false; return false;
} }
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
return room.currentState.maySendStateEvent('im.vector.modular.widgets', me); return room.currentState.maySendStateEvent('im.vector.modular.widgets', me);
} }
@ -182,6 +183,7 @@ export default class WidgetUtils {
} }
const room = MatrixClientPeg.get().getRoom(roomId); const room = MatrixClientPeg.get().getRoom(roomId);
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
if (eventsInIntendedState(startingWidgetEvents)) { if (eventsInIntendedState(startingWidgetEvents)) {
resolve(); resolve();
@ -191,6 +193,7 @@ export default class WidgetUtils {
function onRoomStateEvents(ev) { function onRoomStateEvents(ev) {
if (ev.getRoomId() !== roomId) return; if (ev.getRoomId() !== roomId) return;
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
if (eventsInIntendedState(currentWidgetEvents)) { if (eventsInIntendedState(currentWidgetEvents)) {
@ -272,8 +275,7 @@ export default class WidgetUtils {
WidgetEchoStore.setRoomWidgetEcho(roomId, widgetId, content); WidgetEchoStore.setRoomWidgetEcho(roomId, widgetId, content);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
// TODO - Room widgets need to be moved to 'm.widget' state events // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
return client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).then(() => { return client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).then(() => {
return WidgetUtils.waitForRoomWidget(widgetId, roomId, addingWidget); return WidgetUtils.waitForRoomWidget(widgetId, roomId, addingWidget);
}).finally(() => { }).finally(() => {
@ -287,6 +289,7 @@ export default class WidgetUtils {
* @return {[object]} Array containing current / active room widgets * @return {[object]} Array containing current / active room widgets
*/ */
static getRoomWidgets(room: Room) { static getRoomWidgets(room: Room) {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
if (!appsStateEvents) { if (!appsStateEvents) {
return []; return [];

View file

@ -61,15 +61,18 @@ function UntrustedDeviceDialog(props) {
} }
export async function verifyDevice(user, device) { export async function verifyDevice(user, device) {
if (!await enable4SIfNeeded()) { const cli = MatrixClientPeg.get();
return; // if cross-signing is not explicitly disabled, check if it should be enabled first.
if (cli.getCryptoTrustCrossSignedDevices()) {
if (!await enable4SIfNeeded()) {
return;
}
} }
Modal.createTrackedDialog("Verification warning", "unverified session", UntrustedDeviceDialog, { Modal.createTrackedDialog("Verification warning", "unverified session", UntrustedDeviceDialog, {
user, user,
device, device,
onFinished: async (action) => { onFinished: async (action) => {
if (action === "sas") { if (action === "sas") {
const cli = MatrixClientPeg.get();
const verificationRequestPromise = cli.legacyDeviceVerification( const verificationRequestPromise = cli.legacyDeviceVerification(
user.userId, user.userId,
device.deviceId, device.deviceId,
@ -95,10 +98,13 @@ export async function verifyDevice(user, device) {
} }
export async function legacyVerifyUser(user) { export async function legacyVerifyUser(user) {
if (!await enable4SIfNeeded()) {
return;
}
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
// if cross-signing is not explicitly disabled, check if it should be enabled first.
if (cli.getCryptoTrustCrossSignedDevices()) {
if (!await enable4SIfNeeded()) {
return;
}
}
const verificationRequestPromise = cli.requestVerification(user.userId); const verificationRequestPromise = cli.requestVerification(user.userId);
dis.dispatch({ dis.dispatch({
action: "set_right_panel_phase", action: "set_right_panel_phase",

View file

@ -177,6 +177,7 @@ describe.skip('RoomSettings', () => {
'm.room.history_visibility': 50, 'm.room.history_visibility': 50,
'm.room.power_levels': 50, 'm.room.power_levels': 50,
'm.room.topic': 50, 'm.room.topic': 50,
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
'im.vector.modular.widgets': 50, 'im.vector.modular.widgets': 50,
}, },
}, },

View file

@ -2270,11 +2270,6 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17"
integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==
chain-function@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.1.tgz#c63045e5b4b663fb86f1c6e186adaf1de402a1cc"
integrity sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==
chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -2947,13 +2942,6 @@ doctrine@^3.0.0:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dom-helpers@^3.2.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
dom-serializer@0, dom-serializer@^0.2.1: dom-serializer@0, dom-serializer@^0.2.1:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@ -6775,7 +6763,7 @@ prop-types-exact@^1.2.0:
object.assign "^4.1.0" object.assign "^4.1.0"
reflect.ownkeys "^0.2.0" reflect.ownkeys "^0.2.0"
prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -6958,13 +6946,6 @@ rc@1.2.8, rc@^1.2.8:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-addons-css-transition-group@15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.6.2.tgz#9e4376bcf40b5217d14ec68553081cee4b08a6d6"
integrity sha1-nkN2vPQLUhfRTsaFUwgc7ksIptY=
dependencies:
react-transition-group "^1.2.0"
react-beautiful-dnd@^4.0.1: react-beautiful-dnd@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81"
@ -7052,17 +7033,6 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0:
react-is "^16.8.6" react-is "^16.8.6"
scheduler "^0.19.1" scheduler "^0.19.1"
react-transition-group@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6"
integrity sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==
dependencies:
chain-function "^1.0.0"
dom-helpers "^3.2.0"
loose-envify "^1.3.1"
prop-types "^15.5.6"
warning "^3.0.0"
react@^16.9.0: react@^16.9.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
@ -8803,13 +8773,6 @@ walker@^1.0.7, walker@~1.0.5:
dependencies: dependencies:
makeerror "1.0.x" makeerror "1.0.x"
warning@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=
dependencies:
loose-envify "^1.0.0"
watchpack@^1.6.0: watchpack@^1.6.0:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2"