Merge branches 'develop' and 't3chguy/edit_skip_if_no_edit' of github.com:matrix-org/matrix-react-sdk into t3chguy/edit_skip_if_no_edit
This commit is contained in:
commit
bf9353f3af
122 changed files with 3020 additions and 1327 deletions
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,7 +16,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -48,7 +48,6 @@ export default class ContextualMenu extends React.Component {
|
|||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
menuColour: PropTypes.string,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func,
|
||||
|
@ -157,25 +156,6 @@ export default class ContextualMenu extends React.Component {
|
|||
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||
}
|
||||
|
||||
// To override the default chevron colour, if it's been set
|
||||
let chevronCSS = "";
|
||||
if (props.menuColour) {
|
||||
chevronCSS = `
|
||||
.mx_ContextualMenu_chevron_left:after {
|
||||
border-right-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_right:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_top:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_bottom:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const chevron = hasChevron ?
|
||||
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
|
||||
undefined;
|
||||
|
@ -202,10 +182,6 @@ export default class ContextualMenu extends React.Component {
|
|||
menuStyle.height = props.menuHeight;
|
||||
}
|
||||
|
||||
if (props.menuColour) {
|
||||
menuStyle["backgroundColor"] = props.menuColour;
|
||||
}
|
||||
|
||||
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||
}
|
||||
|
@ -236,7 +212,6 @@ export default class ContextualMenu extends React.Component {
|
|||
</div>
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
|
||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2017, 2018 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -861,9 +862,9 @@ export default React.createClass({
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
|
||||
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||
|
||||
const roomsHelpNode = this.state.editing ? <ToolTipButton helpText={
|
||||
const roomsHelpNode = this.state.editing ? <TooltipButton helpText={
|
||||
_t(
|
||||
'These rooms are displayed to community members on the community page. '+
|
||||
'Community members can join the rooms by clicking on them.',
|
||||
|
|
|
@ -38,6 +38,8 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
this.checkOverflow = this.checkOverflow.bind(this);
|
||||
this._scrollElement = null;
|
||||
this._autoHideScrollbar = null;
|
||||
this._likelyTrackpadUser = null;
|
||||
this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||
|
||||
this.state = {
|
||||
leftIndicatorOffset: 0,
|
||||
|
@ -129,7 +131,27 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
|
||||
const yRetention = 1.0;
|
||||
|
||||
if (Math.abs(e.deltaX) <= xyThreshold) {
|
||||
// whenever we see horizontal scrolling, assume the user is on a trackpad
|
||||
// for at least the next 1 minute.
|
||||
const now = new Date().getTime();
|
||||
if (Math.abs(e.deltaX) > 0) {
|
||||
this._likelyTrackpadUser = true;
|
||||
this._checkAgainForTrackpad = now + (1 * 60 * 1000);
|
||||
} else {
|
||||
// if we haven't seen any horizontal scrolling for a while, assume
|
||||
// the user might have plugged in a mousewheel
|
||||
if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) {
|
||||
this._likelyTrackpadUser = false;
|
||||
}
|
||||
}
|
||||
|
||||
// don't mess with the horizontal scroll for trackpad users
|
||||
// See https://github.com/vector-im/riot-web/issues/10005
|
||||
if (this._likelyTrackpadUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(e.deltaX) <= xyThreshold) { // we are vertically scrolling.
|
||||
// HACK: We increase the amount of scroll to counteract smooth scrolling browsers.
|
||||
// Smooth scrolling browsers (Firefox) use the relative area to determine the scroll
|
||||
// amount, which means the likely small area of content results in a small amount of
|
||||
|
|
|
@ -292,16 +292,6 @@ const LoggedInView = React.createClass({
|
|||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
const action = ev.keyCode == KeyCode.UP ?
|
||||
'view_prev_room' : 'view_next_room';
|
||||
dis.dispatch({action: action});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.PAGE_UP:
|
||||
case KeyCode.PAGE_DOWN:
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
|
|
|
@ -53,6 +53,8 @@ import { messageForSyncError } from '../../utils/ErrorUtils';
|
|||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||
import DMRoomMap from '../../utils/DMRoomMap';
|
||||
import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||
|
||||
// Disable warnings for now: we use deprecated bluebird functions
|
||||
// and need to migrate, but they spam the console with warnings.
|
||||
|
@ -887,6 +889,7 @@ export default React.createClass({
|
|||
}
|
||||
return;
|
||||
}
|
||||
MatrixClientPeg.setJustRegisteredUserId(credentials.user_id);
|
||||
this.onRegistered(credentials);
|
||||
},
|
||||
onDifferentServerClicked: (ev) => {
|
||||
|
@ -1131,29 +1134,81 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts a chat with the welcome user, if the user doesn't already have one
|
||||
* @returns {string} The room ID of the new room, or null if no room was created
|
||||
*/
|
||||
async _startWelcomeUserChat() {
|
||||
// We can end up with multiple tabs post-registration where the user
|
||||
// might then end up with a session and we don't want them all making
|
||||
// a chat with the welcome user: try to de-dupe.
|
||||
// We need to wait for the first sync to complete for this to
|
||||
// work though.
|
||||
let waitFor;
|
||||
if (!this.firstSyncComplete) {
|
||||
waitFor = this.firstSyncPromise.promise;
|
||||
} else {
|
||||
waitFor = Promise.resolve();
|
||||
}
|
||||
await waitFor;
|
||||
|
||||
const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId(
|
||||
this.props.config.welcomeUserId,
|
||||
);
|
||||
if (welcomeUserRooms.length === 0) {
|
||||
const roomId = await createRoom({
|
||||
dmUserId: this.props.config.welcomeUserId,
|
||||
// Only view the welcome user if we're NOT looking at a room
|
||||
andView: !this.state.currentRoomId,
|
||||
spinner: false, // we're already showing one: we don't need another one
|
||||
});
|
||||
// This is a bit of a hack, but since the deduplication relies
|
||||
// on m.direct being up to date, we need to force a sync
|
||||
// of the database, otherwise if the user goes to the other
|
||||
// tab before the next save happens (a few minutes), the
|
||||
// saved sync will be restored from the db and this code will
|
||||
// run without the update to m.direct, making another welcome
|
||||
// user room (it doesn't wait for new data from the server, just
|
||||
// the saved sync to be loaded).
|
||||
const saveWelcomeUser = (ev) => {
|
||||
if (
|
||||
ev.getType() == 'm.direct' &&
|
||||
ev.getContent() &&
|
||||
ev.getContent()[this.props.config.welcomeUserId]
|
||||
) {
|
||||
MatrixClientPeg.get().store.save(true);
|
||||
MatrixClientPeg.get().removeListener(
|
||||
"accountData", saveWelcomeUser,
|
||||
);
|
||||
}
|
||||
};
|
||||
MatrixClientPeg.get().on("accountData", saveWelcomeUser);
|
||||
|
||||
return roomId;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a new logged in session has started
|
||||
*/
|
||||
_onLoggedIn: async function() {
|
||||
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
||||
if (this._is_registered) {
|
||||
this._is_registered = false;
|
||||
if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||
MatrixClientPeg.setJustRegisteredUserId(null);
|
||||
|
||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
const roomId = await createRoom({
|
||||
dmUserId: this.props.config.welcomeUserId,
|
||||
// Only view the welcome user if we're NOT looking at a room
|
||||
andView: !this.state.currentRoomId,
|
||||
});
|
||||
// if successful, return because we're already
|
||||
// viewing the welcomeUserId room
|
||||
// else, if failed, fall through to view_home_page
|
||||
if (roomId) {
|
||||
return;
|
||||
const welcomeUserRoom = await this._startWelcomeUserChat();
|
||||
if (welcomeUserRoom === null) {
|
||||
// We didn't rediret to the welcome user room, so show
|
||||
// the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
} else {
|
||||
// The user has just logged in after registering,
|
||||
// so show the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
// The user has just logged in after registering
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else {
|
||||
this._showScreenAfterLogin();
|
||||
}
|
||||
|
@ -1655,48 +1710,6 @@ export default React.createClass({
|
|||
|
||||
// returns a promise which resolves to the new MatrixClient
|
||||
onRegistered: function(credentials) {
|
||||
if (this.state.register_session_id) {
|
||||
// The user came in through an email validation link. To avoid overwriting
|
||||
// their session, check to make sure the session isn't someone else, and
|
||||
// isn't a guest user since we'll usually have set a guest user session before
|
||||
// starting the registration process. This isn't perfect since it's possible
|
||||
// the user had a separate guest session they didn't actually mean to replace.
|
||||
const sessionOwner = Lifecycle.getStoredSessionOwner();
|
||||
const sessionIsGuest = Lifecycle.getStoredSessionIsGuest();
|
||||
if (sessionOwner && !sessionIsGuest && sessionOwner !== credentials.userId) {
|
||||
console.log(
|
||||
`Found a session for ${sessionOwner} but ${credentials.userId} is trying to verify their ` +
|
||||
`email address. Restoring the session for ${sessionOwner} with warning.`,
|
||||
);
|
||||
this._loadSession();
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
// N.B. first param is passed to piwik and so doesn't want i18n
|
||||
Modal.createTrackedDialog('Existing session on register', '',
|
||||
QuestionDialog, {
|
||||
title: _t('You are logged in to another account'),
|
||||
description: _t(
|
||||
"Thank you for verifying your email! The account you're logged into here " +
|
||||
"(%(sessionUserId)s) appears to be different from the account you've verified an " +
|
||||
"email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, " +
|
||||
"please log out first.", {
|
||||
sessionUserId: sessionOwner,
|
||||
verifiedUserId: credentials.userId,
|
||||
|
||||
// TODO: Fix translations to support reusing variables.
|
||||
// https://github.com/vector-im/riot-web/issues/9086
|
||||
verifiedUserId2: credentials.userId,
|
||||
},
|
||||
),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
|
||||
return MatrixClientPeg.get();
|
||||
}
|
||||
}
|
||||
// XXX: This should be in state or ideally store(s) because we risk not
|
||||
// rendering the most up-to-date view of state otherwise.
|
||||
this._is_registered = true;
|
||||
return Lifecycle.setLoggedIn(credentials);
|
||||
},
|
||||
|
||||
|
@ -1737,19 +1750,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
updateStatusIndicator: function(state, prevState) {
|
||||
let notifCount = 0;
|
||||
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
for (let i = 0; i < rooms.length; ++i) {
|
||||
if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) {
|
||||
notifCount++;
|
||||
} else if (rooms[i].getUnreadNotificationCount()) {
|
||||
// if we were summing unread notifs:
|
||||
// notifCount += rooms[i].getUnreadNotificationCount();
|
||||
// instead, we just count the number of rooms with notifs.
|
||||
notifCount++;
|
||||
}
|
||||
}
|
||||
const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getRooms()).count;
|
||||
|
||||
if (PlatformPeg.get()) {
|
||||
PlatformPeg.get().setErrorStatus(state === 'ERROR');
|
||||
|
|
|
@ -108,6 +108,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||
// the event after which we put a visible unread marker on the last
|
||||
// render cycle; null if readMarkerVisible was false or the RM was
|
||||
// suppressed (eg because it was at the end of the timeline)
|
||||
|
@ -585,14 +586,14 @@ module.exports = React.createClass({
|
|||
<EventTile mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
replacingEventId={this._editingEnabled && mxEv.replacingEventId()}
|
||||
editState={isEditing && this.props.editState}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.replacementOrOwnStatus()}
|
||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
|
|
|
@ -333,7 +333,7 @@ module.exports = React.createClass({
|
|||
if (alias.indexOf(':') == -1) {
|
||||
alias = alias + ':' + this.state.roomServer;
|
||||
}
|
||||
this.showRoomAlias(alias);
|
||||
this.showRoomAlias(alias, true);
|
||||
} else {
|
||||
// This is a 3rd party protocol. Let's see if we can join it
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
|
@ -349,7 +349,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
|
||||
if (resp.length > 0 && resp[0].alias) {
|
||||
this.showRoomAlias(resp[0].alias);
|
||||
this.showRoomAlias(resp[0].alias, true);
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||
|
@ -367,13 +367,16 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
showRoomAlias: function(alias) {
|
||||
this.showRoom(null, alias);
|
||||
showRoomAlias: function(alias, autoJoin=false) {
|
||||
this.showRoom(null, alias, autoJoin);
|
||||
},
|
||||
|
||||
showRoom: function(room, room_alias) {
|
||||
showRoom: function(room, room_alias, autoJoin=false) {
|
||||
this.props.onFinished();
|
||||
const payload = {action: 'view_room'};
|
||||
const payload = {
|
||||
action: 'view_room',
|
||||
auto_join: autoJoin,
|
||||
};
|
||||
if (room) {
|
||||
// Don't let the user view a room they won't be able to either
|
||||
// peek or join: fail earlier so they don't have to click back
|
||||
|
|
|
@ -1523,9 +1523,11 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomView">
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
oobData={this.props.oobData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -29,6 +29,7 @@ import * as ServerType from '../../views/auth/ServerTypeSelector';
|
|||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
|
@ -96,6 +97,13 @@ module.exports = React.createClass({
|
|||
// Our matrix client - part of state because we can't render the UI auth
|
||||
// component without it.
|
||||
matrixClient: null,
|
||||
|
||||
// The user ID we've just registered
|
||||
registeredUsername: null,
|
||||
|
||||
// if a different user ID to the one we just registered is logged in,
|
||||
// this is the user ID that's logged in.
|
||||
differentLoggedInUserId: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -167,6 +175,8 @@ module.exports = React.createClass({
|
|||
_replaceClient: async function(serverConfig) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
serverDeadError: null,
|
||||
serverErrorIsFatal: false,
|
||||
// busy while we do liveness check (we need to avoid trying to render
|
||||
// the UI auth component while we don't have a matrix client)
|
||||
busy: true,
|
||||
|
@ -179,7 +189,10 @@ module.exports = React.createClass({
|
|||
serverConfig.hsUrl,
|
||||
serverConfig.isUrl,
|
||||
);
|
||||
this.setState({serverIsAlive: true});
|
||||
this.setState({
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
|
@ -287,9 +300,29 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
MatrixClientPeg.setJustRegisteredUserId(response.user_id);
|
||||
|
||||
const newState = {
|
||||
doingUIAuth: false,
|
||||
registeredUsername: response.user_id,
|
||||
};
|
||||
|
||||
// The user came in through an email validation link. To avoid overwriting
|
||||
// their session, check to make sure the session isn't someone else, and
|
||||
// isn't a guest user since we'll usually have set a guest user session before
|
||||
// starting the registration process. This isn't perfect since it's possible
|
||||
// the user had a separate guest session they didn't actually mean to replace.
|
||||
const sessionOwner = Lifecycle.getStoredSessionOwner();
|
||||
const sessionIsGuest = Lifecycle.getStoredSessionIsGuest();
|
||||
if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) {
|
||||
console.log(
|
||||
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
|
||||
);
|
||||
newState.differentLoggedInUserId = sessionOwner;
|
||||
} else {
|
||||
newState.differentLoggedInUserId = null;
|
||||
}
|
||||
|
||||
if (response.access_token) {
|
||||
const cli = await this.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
|
@ -423,7 +456,9 @@ module.exports = React.createClass({
|
|||
|
||||
// If we're on a different phase, we only show the server type selector,
|
||||
// which is always shown if we allow custom URLs at all.
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
||||
// (if there's a fatal server error, we need to show the full server
|
||||
// config as the user may need to change servers to resolve the error).
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) {
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
|
@ -528,6 +563,7 @@ module.exports = React.createClass({
|
|||
const AuthHeader = sdk.getComponent('auth.AuthHeader');
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const AuthPage = sdk.getComponent('auth.AuthPage');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let errorText;
|
||||
const err = this.state.errorText;
|
||||
|
@ -564,28 +600,41 @@ module.exports = React.createClass({
|
|||
let body;
|
||||
if (this.state.completedNoSignin) {
|
||||
let regDoneText;
|
||||
if (this.state.formVals.password) {
|
||||
if (this.state.differentLoggedInUserId) {
|
||||
regDoneText = <div>
|
||||
<p>{_t(
|
||||
"Your new account (%(newAccountId)s) is registered, but you're already " +
|
||||
"logged into a different account (%(loggedInUserId)s).", {
|
||||
newAccountId: this.state.registeredUsername,
|
||||
loggedInUserId: this.state.differentLoggedInUserId,
|
||||
},
|
||||
)}</p>
|
||||
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this._onLoginClickWithCheck}>
|
||||
{_t("Continue with previous account")}
|
||||
</AccessibleButton></p>
|
||||
</div>;
|
||||
} else if (this.state.formVals.password) {
|
||||
// We're the client that started the registration
|
||||
regDoneText = _t(
|
||||
regDoneText = <h3>{_t(
|
||||
"<a>Log in</a> to your new account.", {},
|
||||
{
|
||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||
},
|
||||
);
|
||||
)}</h3>;
|
||||
} else {
|
||||
// We're not the original client: the user probably got to us by clicking the
|
||||
// email validation link. We can't offer a 'go straight to your account' link
|
||||
// as we don't have the original creds.
|
||||
regDoneText = _t(
|
||||
regDoneText = <h3>{_t(
|
||||
"You can now close this window or <a>log in</a> to your new account.", {},
|
||||
{
|
||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||
},
|
||||
);
|
||||
)}</h3>;
|
||||
}
|
||||
body = <div>
|
||||
<h2>{_t("Registration Successful")}</h2>
|
||||
<h3>{ regDoneText }</h3>
|
||||
{ regDoneText }
|
||||
</div>;
|
||||
} else {
|
||||
body = <div>
|
||||
|
|
|
@ -81,16 +81,10 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
passwordValid: false,
|
||||
password: "",
|
||||
};
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.refs.passwordField) {
|
||||
this.refs.passwordField.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.busy) return;
|
||||
|
@ -98,23 +92,21 @@ export const PasswordAuthEntry = React.createClass({
|
|||
this.props.submitAuthDict({
|
||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||
user: this.props.matrixClient.credentials.userId,
|
||||
password: this.refs.passwordField.value,
|
||||
password: this.state.password,
|
||||
});
|
||||
},
|
||||
|
||||
_onPasswordFieldChange: function(ev) {
|
||||
// enable the submit button iff the password is non-empty
|
||||
this.setState({
|
||||
passwordValid: Boolean(this.refs.passwordField.value),
|
||||
password: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let passwordBoxClass = null;
|
||||
|
||||
if (this.props.errorText) {
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
const passwordBoxClass = classnames({
|
||||
"error": this.props.errorText,
|
||||
});
|
||||
|
||||
let submitButtonOrSpinner;
|
||||
if (this.props.busy) {
|
||||
|
@ -124,7 +116,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||
submitButtonOrSpinner = (
|
||||
<input type="submit"
|
||||
className="mx_Dialog_primary"
|
||||
disabled={!this.state.passwordValid}
|
||||
disabled={!this.state.password}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -138,17 +130,21 @@ export const PasswordAuthEntry = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
<form onSubmit={this._onSubmit}>
|
||||
<label htmlFor="passwordField">{ _t("Password:") }</label>
|
||||
<input
|
||||
name="passwordField"
|
||||
ref="passwordField"
|
||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<Field
|
||||
id="mx_InteractiveAuthEntryComponents_password"
|
||||
className={passwordBoxClass}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
type="password"
|
||||
name="passwordField"
|
||||
label={_t('Password')}
|
||||
autoFocus={true}
|
||||
value={this.state.password}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
/>
|
||||
<div className="mx_button_row">
|
||||
{ submitButtonOrSpinner }
|
||||
|
|
|
@ -104,6 +104,9 @@ export default class ServerConfig extends React.PureComponent {
|
|||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (!stateForError.isFatalError) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
// carry on anyway
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
|
||||
this.props.onServerConfigChange(result);
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
confirmButtonEnabled: false,
|
||||
password: "",
|
||||
busy: false,
|
||||
shouldErase: false,
|
||||
errStr: null,
|
||||
|
@ -45,7 +45,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
|
||||
_onPasswordFieldChange(ev) {
|
||||
this.setState({
|
||||
confirmButtonEnabled: Boolean(ev.target.value),
|
||||
password: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
}
|
||||
|
||||
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
|
||||
const okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
|
||||
const okEnabled = this.state.password && !this.state.busy;
|
||||
|
||||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
|
@ -113,6 +113,8 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
</button>;
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
|
@ -167,10 +169,12 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password:") }</p>
|
||||
<input
|
||||
<Field
|
||||
id="mx_DeactivateAccountDialog_password"
|
||||
type="password"
|
||||
placeholder={_t("password")}
|
||||
label={_t('Password')}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
value={this.state.password}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
|
|
|
@ -34,9 +34,15 @@ export default class IncomingSasDialog extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let phase = PHASE_START;
|
||||
if (this.props.verifier.cancelled) {
|
||||
console.log("Verifier was cancelled in the background.");
|
||||
phase = PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
this._showSasEvent = null;
|
||||
this.state = {
|
||||
phase: PHASE_START,
|
||||
phase: phase,
|
||||
sasVerified: false,
|
||||
opponentProfile: null,
|
||||
opponentProfileError: null,
|
||||
|
|
108
src/components/views/dialogs/MessageEditHistoryDialog.js
Normal file
108
src/components/views/dialogs/MessageEditHistoryDialog.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from "../../../index";
|
||||
import {wantsDateSeparator} from '../../../DateUtils';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
events: [],
|
||||
nextBatch: null,
|
||||
isLoading: true,
|
||||
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||
};
|
||||
}
|
||||
|
||||
loadMoreEdits = async (backwards) => {
|
||||
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
|
||||
// bail out on backwards as we only paginate in one direction
|
||||
return false;
|
||||
}
|
||||
const opts = {from: this.state.nextBatch};
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
const result = await MatrixClientPeg.get().relations(
|
||||
roomId, eventId, "m.replace", "m.room.message", opts);
|
||||
let resolve;
|
||||
const promise = new Promise(r => resolve = r);
|
||||
this.setState({
|
||||
events: this.state.events.concat(result.events),
|
||||
nextBatch: result.nextBatch,
|
||||
isLoading: false,
|
||||
}, () => {
|
||||
const hasMoreResults = !!this.state.nextBatch;
|
||||
resolve(hasMoreResults);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadMoreEdits();
|
||||
}
|
||||
|
||||
_renderEdits() {
|
||||
const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const nodes = [];
|
||||
let lastEvent;
|
||||
this.state.events.forEach(e => {
|
||||
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
|
||||
nodes.push(<li key={e.getTs() + "~"}><DateSeparator ts={e.getTs()} /></li>);
|
||||
}
|
||||
nodes.push(<EditHistoryMessage key={e.getId()} mxEvent={e} isTwelveHour={this.state.isTwelveHour} />);
|
||||
lastEvent = e;
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = this.state.error;
|
||||
} else if (this.state.isLoading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = (<ScrollPanel
|
||||
className="mx_MessageEditHistoryDialog_scrollPanel"
|
||||
onFillRequest={ this.loadMoreEdits }
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
||||
</ScrollPanel>);
|
||||
}
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_MessageEditHistoryDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished} title={_t("Message edits")}>
|
||||
{content}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -92,7 +92,7 @@ export default React.createClass({
|
|||
<p>
|
||||
{_t(
|
||||
"Upgrading this room requires closing down the current " +
|
||||
"instance of the room and creating a new room it its place. " +
|
||||
"instance of the room and creating a new room in its place. " +
|
||||
"To give room members the best possible experience, we will:",
|
||||
)}
|
||||
</p>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,6 +19,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import filesize from "filesize";
|
||||
|
||||
export default class UploadConfirmDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -49,6 +51,10 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_onUploadAllClick = () => {
|
||||
this.props.onFinished(true, true);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
@ -71,7 +77,7 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
||||
<div className="mx_UploadConfirmDialog_previewInner">
|
||||
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
|
||||
<div>{this.props.file.name}</div>
|
||||
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
|
@ -80,11 +86,18 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
<img className="mx_UploadConfirmDialog_fileIcon"
|
||||
src={require("../../../../res/img/files.png")}
|
||||
/>
|
||||
{this.props.file.name}
|
||||
{this.props.file.name} ({filesize(this.props.file.size)})
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let uploadAllButton;
|
||||
if (this.props.currentIndex + 1 < this.props.totalFiles) {
|
||||
uploadAllButton = <button onClick={this._onUploadAllClick}>
|
||||
{_t("Upload all")}
|
||||
</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_UploadConfirmDialog'
|
||||
fixedWidth={false}
|
||||
|
@ -100,7 +113,9 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onUploadClick}
|
||||
focus={true}
|
||||
/>
|
||||
>
|
||||
{uploadAllButton}
|
||||
</DialogButtons>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -240,19 +240,13 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
// The dialog handles scalar auth for us
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
this._scalarClient.connect().done(() => {
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room, 'type_' + this.props.type, this.props.id);
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
error: err.message,
|
||||
});
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
room: this.props.room,
|
||||
screen: 'type_' + this.props.type,
|
||||
integrationId: this.props.id,
|
||||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
195
src/components/views/elements/InteractiveTooltip.js
Normal file
195
src/components/views/elements/InteractiveTooltip.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container";
|
||||
|
||||
// If the distance from tooltip to window edge is below this value, the tooltip
|
||||
// will flip around to the other side of the target.
|
||||
const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20;
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(InteractiveTooltipContainerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = InteractiveTooltipContainerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function isInRect(x, y, rect, buffer = 10) {
|
||||
const { top, right, bottom, left } = rect;
|
||||
return x >= (left - buffer) && x <= (right + buffer)
|
||||
&& y >= (top - buffer) && y <= (bottom + buffer);
|
||||
}
|
||||
|
||||
/*
|
||||
* This style of tooltip takes a "target" element as its child and centers the
|
||||
* tooltip along one edge of the target.
|
||||
*/
|
||||
export default class InteractiveTooltip extends React.Component {
|
||||
propTypes: {
|
||||
// Content to show in the tooltip
|
||||
content: PropTypes.node.isRequired,
|
||||
// Function to call when visibility of the tooltip changes
|
||||
onVisibilityChange: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
contentRect: null,
|
||||
visible: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Whenever this passthrough component updates, also render the tooltip
|
||||
// in a separate DOM tree. This allows the tooltip content to participate
|
||||
// the normal React rendering cycle: when this component re-renders, the
|
||||
// tooltip content re-renders.
|
||||
// Once we upgrade to React 16, this could be done a bit more naturally
|
||||
// using the portals feature instead.
|
||||
this.renderTooltip();
|
||||
}
|
||||
|
||||
collectContentRect = (element) => {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
this.setState({
|
||||
contentRect: element.getBoundingClientRect(),
|
||||
});
|
||||
}
|
||||
|
||||
collectTarget = (element) => {
|
||||
this.target = element;
|
||||
}
|
||||
|
||||
onBackgroundClick = (ev) => {
|
||||
this.hideTooltip();
|
||||
}
|
||||
|
||||
onBackgroundMouseMove = (ev) => {
|
||||
const { clientX: x, clientY: y } = ev;
|
||||
const { contentRect } = this.state;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
if (!isInRect(x, y, contentRect) && !isInRect(x, y, targetRect)) {
|
||||
this.hideTooltip();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onTargetMouseOver = (ev) => {
|
||||
this.showTooltip();
|
||||
}
|
||||
|
||||
showTooltip() {
|
||||
this.setState({
|
||||
visible: true,
|
||||
});
|
||||
if (this.props.onVisibilityChange) {
|
||||
this.props.onVisibilityChange(true);
|
||||
}
|
||||
}
|
||||
|
||||
hideTooltip() {
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
if (this.props.onVisibilityChange) {
|
||||
this.props.onVisibilityChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
renderTooltip() {
|
||||
const { contentRect, visible } = this.state;
|
||||
if (!visible) {
|
||||
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const targetLeft = targetRect.left + window.pageXOffset;
|
||||
const targetBottom = targetRect.bottom + window.pageYOffset;
|
||||
const targetTop = targetRect.top + window.pageYOffset;
|
||||
|
||||
// Place the tooltip above the target by default. If we find that the
|
||||
// tooltip content would extend past the safe area towards the window
|
||||
// edge, flip around to below the target.
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
if (contentRect && (targetTop - contentRect.height <= MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)) {
|
||||
position.top = targetBottom;
|
||||
chevronFace = "top";
|
||||
} else {
|
||||
position.bottom = window.innerHeight - targetTop;
|
||||
chevronFace = "bottom";
|
||||
}
|
||||
|
||||
// Center the tooltip horizontally with the target's center.
|
||||
position.left = targetLeft + targetRect.width / 2;
|
||||
|
||||
const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
|
||||
|
||||
const menuClasses = classNames({
|
||||
'mx_InteractiveTooltip': true,
|
||||
'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top',
|
||||
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom',
|
||||
});
|
||||
|
||||
const menuStyle = {};
|
||||
if (contentRect) {
|
||||
menuStyle.left = `-${contentRect.width / 2}px`;
|
||||
}
|
||||
|
||||
const tooltip = <div className="mx_InteractiveTooltip_wrapper" style={{...position}}>
|
||||
<div className="mx_ContextualMenu_background"
|
||||
onMouseMove={this.onBackgroundMouseMove}
|
||||
onClick={this.onBackgroundClick}
|
||||
/>
|
||||
<div className={menuClasses}
|
||||
style={menuStyle}
|
||||
ref={this.collectContentRect}
|
||||
>
|
||||
{chevron}
|
||||
{this.props.content}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(tooltip, getOrCreateContainer());
|
||||
}
|
||||
|
||||
render() {
|
||||
// We use `cloneElement` here to append some props to the child content
|
||||
// without using a wrapper element which could disrupt layout.
|
||||
return React.cloneElement(this.props.children, {
|
||||
ref: this.collectTarget,
|
||||
onMouseOver: this.onTargetMouseOver,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,95 +18,34 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import Modal from "../../../Modal";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
|
||||
export default class ManageIntegsButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
scalarError: null,
|
||||
};
|
||||
|
||||
this.onManageIntegrations = this.onManageIntegrations.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
ScalarMessaging.startListening();
|
||||
this.scalarClient = null;
|
||||
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({scalarError: err});
|
||||
console.error('Error whilst initialising scalarClient for ManageIntegsButton', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ScalarMessaging.stopListening();
|
||||
}
|
||||
|
||||
onManageIntegrations(ev) {
|
||||
onManageIntegrations = (ev) => {
|
||||
ev.preventDefault();
|
||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
this.scalarClient.connect().done(() => {
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
|
||||
null,
|
||||
}, "mx_IntegrationsManager");
|
||||
}, (err) => {
|
||||
this.setState({scalarError: err});
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
}
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
room: this.props.room,
|
||||
}, "mx_IntegrationsManager");
|
||||
};
|
||||
|
||||
render() {
|
||||
let integrationsButton = <div />;
|
||||
let integrationsWarningTriangle = <div />;
|
||||
let integrationsErrorPopup = <div />;
|
||||
if (this.scalarClient !== null) {
|
||||
const integrationsButtonClasses = classNames({
|
||||
mx_RoomHeader_button: true,
|
||||
mx_RoomHeader_manageIntegsButton: true,
|
||||
mx_ManageIntegsButton_error: !!this.state.scalarError,
|
||||
});
|
||||
|
||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||
integrationsWarningTriangle = <img
|
||||
src={require("../../../../res/img/warning.svg")}
|
||||
title={_t('Integrations Error')}
|
||||
width="17"
|
||||
/>;
|
||||
// Popup shown when hovering over integrationsButton_error (via CSS)
|
||||
integrationsErrorPopup = (
|
||||
<span className="mx_ManageIntegsButton_errorPopup">
|
||||
{ _t('Could not connect to the integration server') }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (ScalarAuthClient.isPossible()) {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
integrationsButton = (
|
||||
<AccessibleButton className={integrationsButtonClasses}
|
||||
<AccessibleButton
|
||||
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
|
||||
title={_t("Manage Integrations")}
|
||||
onClick={this.onManageIntegrations}
|
||||
title={_t('Manage Integrations')}
|
||||
>
|
||||
{ integrationsWarningTriangle }
|
||||
{ integrationsErrorPopup }
|
||||
</AccessibleButton>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,14 @@ export default class MessageEditor extends React.Component {
|
|||
this.model.update(text, event.inputType, caret);
|
||||
}
|
||||
|
||||
_insertText(textToInsert, inputType = "insertText") {
|
||||
const sel = document.getSelection();
|
||||
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
||||
const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
|
||||
caret.offset += textToInsert.length;
|
||||
this.model.update(newText, inputType, caret);
|
||||
}
|
||||
|
||||
_isCaretAtStart() {
|
||||
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
||||
return caret.offset === 0;
|
||||
|
@ -92,7 +100,7 @@ export default class MessageEditor extends React.Component {
|
|||
// insert newline on Shift+Enter
|
||||
if (event.shiftKey && event.key === "Enter") {
|
||||
event.preventDefault(); // just in case the browser does support this
|
||||
document.execCommand("insertHTML", undefined, "\n");
|
||||
this._insertText("\n");
|
||||
return;
|
||||
}
|
||||
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||
|
@ -150,16 +158,28 @@ export default class MessageEditor extends React.Component {
|
|||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
|
||||
_isEmote() {
|
||||
const firstPart = this.model.parts[0];
|
||||
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
|
||||
}
|
||||
|
||||
_sendEdit = () => {
|
||||
const isEmote = this._isEmote();
|
||||
let model = this.model;
|
||||
if (isEmote) {
|
||||
// trim "/me "
|
||||
model = model.clone();
|
||||
model.removeText({index: 0, offset: 0}, 4);
|
||||
}
|
||||
const newContent = {
|
||||
"msgtype": "m.text",
|
||||
"body": textSerialize(this.model),
|
||||
"msgtype": isEmote ? "m.emote" : "m.text",
|
||||
"body": textSerialize(model),
|
||||
};
|
||||
const contentBody = {
|
||||
msgtype: newContent.msgtype,
|
||||
body: ` * ${newContent.body}`,
|
||||
};
|
||||
const formattedBody = htmlSerializeIfNeeded(this.model);
|
||||
const formattedBody = htmlSerializeIfNeeded(model);
|
||||
if (formattedBody) {
|
||||
newContent.format = "org.matrix.custom.html";
|
||||
newContent.formatted_body = formattedBody;
|
||||
|
@ -232,7 +252,7 @@ export default class MessageEditor extends React.Component {
|
|||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
} else {
|
||||
// otherwise, parse the body of the event
|
||||
parts = parseEvent(editState.getEvent(), room, this.context.matrixClient);
|
||||
parts = parseEvent(editState.getEvent(), partCreator);
|
||||
}
|
||||
|
||||
return new EditorModel(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,7 +19,7 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ToolTipButton',
|
||||
displayName: 'TooltipButton',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
|
@ -41,12 +42,12 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_ToolTipButton_container"
|
||||
tooltipClassName="mx_ToolTipButton_helpText"
|
||||
className="mx_TooltipButton_container"
|
||||
tooltipClassName="mx_TooltipButton_helpText"
|
||||
label={this.props.helpText}
|
||||
/> : <div />;
|
||||
return (
|
||||
<div className="mx_ToolTipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
||||
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
||||
?
|
||||
{ tip }
|
||||
</div>
|
|
@ -224,7 +224,7 @@ module.exports = React.createClass({
|
|||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.state.groupRoom.canonical_alias }
|
||||
{ this.state.groupRoom.canonicalAlias }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
61
src/components/views/messages/EditHistoryMessage.js
Normal file
61
src/components/views/messages/EditHistoryMessage.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import {formatTime} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
|
||||
export default class EditHistoryMessage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// the message event being edited
|
||||
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {mxEvent} = this.props;
|
||||
const originalContent = mxEvent.getOriginalContent();
|
||||
const content = originalContent["m.new_content"] || originalContent;
|
||||
const contentElements = HtmlUtils.bodyToHtml(content);
|
||||
let contentContainer;
|
||||
if (mxEvent.getContent().msgtype === "m.emote") {
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
contentContainer = (<div className="mx_EventTile_content" ref="content">*
|
||||
<span className="mx_MEmoteBody_sender">{ name }</span>
|
||||
{contentElements}
|
||||
</div>);
|
||||
} else {
|
||||
contentContainer = (<div className="mx_EventTile_content" ref="content">{contentElements}</div>);
|
||||
}
|
||||
const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
|
||||
return <li className="mx_EventTile">
|
||||
<div className="mx_EventTile_line">
|
||||
<span className="mx_MessageTimestamp">{timestamp}</span>
|
||||
{ contentContainer }
|
||||
</div>
|
||||
</li>;
|
||||
}
|
||||
}
|
|
@ -57,7 +57,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
this.props.onFocusChange(focused);
|
||||
}
|
||||
|
||||
onCryptoClicked = () => {
|
||||
onCryptoClick = () => {
|
||||
const event = this.props.mxEvent;
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
|
@ -89,7 +89,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) {
|
||||
e2eInfoCallback = () => this.onCryptoClicked();
|
||||
e2eInfoCallback = () => this.onCryptoClick();
|
||||
}
|
||||
|
||||
const menuOptions = {
|
||||
|
@ -131,43 +131,28 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
return SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||
}
|
||||
|
||||
renderAgreeDimension() {
|
||||
renderReactButton() {
|
||||
if (!this.isReactionsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
|
||||
return <ReactionDimension
|
||||
title={_t("Agree or Disagree")}
|
||||
options={["👍", "👎"]}
|
||||
reactions={this.props.reactions}
|
||||
mxEvent={this.props.mxEvent}
|
||||
/>;
|
||||
}
|
||||
const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction');
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
renderLikeDimension() {
|
||||
if (!this.isReactionsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
|
||||
return <ReactionDimension
|
||||
title={_t("Like or Dislike")}
|
||||
options={["🙂", "😔"]}
|
||||
reactions={this.props.reactions}
|
||||
mxEvent={this.props.mxEvent}
|
||||
return <ReactMessageAction
|
||||
mxEvent={mxEvent}
|
||||
reactions={reactions}
|
||||
onFocusChange={this.onFocusChange}
|
||||
/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let agreeDimensionReactionButtons;
|
||||
let likeDimensionReactionButtons;
|
||||
let reactButton;
|
||||
let replyButton;
|
||||
let editButton;
|
||||
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
agreeDimensionReactionButtons = this.renderAgreeDimension();
|
||||
likeDimensionReactionButtons = this.renderLikeDimension();
|
||||
reactButton = this.renderReactButton();
|
||||
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
|
@ -181,8 +166,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
return <div className="mx_MessageActionBar">
|
||||
{agreeDimensionReactionButtons}
|
||||
{likeDimensionReactionButtons}
|
||||
{reactButton}
|
||||
{replyButton}
|
||||
{editButton}
|
||||
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
||||
|
|
97
src/components/views/messages/ReactMessageAction.js
Normal file
97
src/components/views/messages/ReactMessageAction.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
|
||||
export default class ReactMessageAction extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: PropTypes.object,
|
||||
onFocusChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
}
|
||||
this.props.onFocusChange(focused);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
this.onReactionsChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
// Force a re-render of the tooltip because a change in the reactions
|
||||
// set means the event tile's layout may have changed and possibly
|
||||
// altered the location where the tooltip should be shown.
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
const ReactionsQuickTooltip = sdk.getComponent('messages.ReactionsQuickTooltip');
|
||||
const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip');
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
const content = <ReactionsQuickTooltip
|
||||
mxEvent={mxEvent}
|
||||
reactions={reactions}
|
||||
/>;
|
||||
|
||||
return <InteractiveTooltip
|
||||
content={content}
|
||||
onVisibilityChange={this.onFocusChange}
|
||||
>
|
||||
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
|
||||
</InteractiveTooltip>;
|
||||
}
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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 classNames from 'classnames';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default class ReactionDimension extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// Array of strings containing the emoji for each option
|
||||
options: PropTypes.array.isRequired,
|
||||
title: PropTypes.string,
|
||||
// The Relations model from the JS SDK for reactions
|
||||
reactions: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.getSelection();
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
this.onReactionsChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
this.setState(this.getSelection());
|
||||
}
|
||||
|
||||
getSelection() {
|
||||
const myReactions = this.getMyReactions();
|
||||
if (!myReactions) {
|
||||
return {
|
||||
selectedOption: null,
|
||||
selectedReactionEvent: null,
|
||||
};
|
||||
}
|
||||
const { options } = this.props;
|
||||
let selectedOption = null;
|
||||
let selectedReactionEvent = null;
|
||||
for (const option of options) {
|
||||
const reactionForOption = myReactions.find(mxEvent => {
|
||||
if (mxEvent.isRedacted()) {
|
||||
return false;
|
||||
}
|
||||
return mxEvent.getRelation().key === option;
|
||||
});
|
||||
if (!reactionForOption) {
|
||||
continue;
|
||||
}
|
||||
if (selectedOption) {
|
||||
// If there are multiple selected values (only expected to occur via
|
||||
// non-Riot clients), then act as if none are selected.
|
||||
return {
|
||||
selectedOption: null,
|
||||
selectedReactionEvent: null,
|
||||
};
|
||||
}
|
||||
selectedOption = option;
|
||||
selectedReactionEvent = reactionForOption;
|
||||
}
|
||||
return { selectedOption, selectedReactionEvent };
|
||||
}
|
||||
|
||||
getMyReactions() {
|
||||
const reactions = this.props.reactions;
|
||||
if (!reactions) {
|
||||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
}
|
||||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onOptionClick = (ev) => {
|
||||
const { key } = ev.target.dataset;
|
||||
this.toggleDimension(key);
|
||||
}
|
||||
|
||||
toggleDimension(key) {
|
||||
const { selectedOption, selectedReactionEvent } = this.state;
|
||||
const newSelectedOption = selectedOption !== key ? key : null;
|
||||
this.setState({
|
||||
selectedOption: newSelectedOption,
|
||||
});
|
||||
if (selectedReactionEvent) {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
selectedReactionEvent.getId(),
|
||||
);
|
||||
}
|
||||
if (newSelectedOption) {
|
||||
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": this.props.mxEvent.getId(),
|
||||
"key": newSelectedOption,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedOption } = this.state;
|
||||
const { options } = this.props;
|
||||
|
||||
const items = options.map(option => {
|
||||
const disabled = selectedOption && selectedOption !== option;
|
||||
const classes = classNames({
|
||||
mx_ReactionDimension_disabled: disabled,
|
||||
});
|
||||
return <span key={option}
|
||||
data-key={option}
|
||||
className={classes}
|
||||
onClick={this.onOptionClick}
|
||||
>
|
||||
{option}
|
||||
</span>;
|
||||
});
|
||||
|
||||
return <span className="mx_ReactionDimension"
|
||||
title={this.props.title}
|
||||
aria-hidden={true}
|
||||
>
|
||||
{items}
|
||||
</span>;
|
||||
}
|
||||
}
|
68
src/components/views/messages/ReactionTooltipButton.js
Normal file
68
src/components/views/messages/ReactionTooltipButton.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default class ReactionTooltipButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
myReactionEvent: PropTypes.object,
|
||||
};
|
||||
|
||||
onClick = (ev) => {
|
||||
const { mxEvent, myReactionEvent, content } = this.props;
|
||||
if (myReactionEvent) {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
mxEvent.getRoomId(),
|
||||
myReactionEvent.getId(),
|
||||
);
|
||||
} else {
|
||||
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": mxEvent.getId(),
|
||||
"key": content,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { content, myReactionEvent } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
mx_ReactionTooltipButton: true,
|
||||
mx_ReactionTooltipButton_selected: !!myReactionEvent,
|
||||
});
|
||||
|
||||
return <span className={classes}
|
||||
data-key={content}
|
||||
title={this.props.title}
|
||||
aria-hidden={true}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{content}
|
||||
</span>;
|
||||
}
|
||||
}
|
195
src/components/views/messages/ReactionsQuickTooltip.js
Normal file
195
src/components/views/messages/ReactionsQuickTooltip.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||
|
||||
export default class ReactionsQuickTooltip extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
hoveredItem: null,
|
||||
myReactions: this.getMyReactions(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
this.onReactionsChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
this.setState({
|
||||
myReactions: this.getMyReactions(),
|
||||
});
|
||||
}
|
||||
|
||||
getMyReactions() {
|
||||
const reactions = this.props.reactions;
|
||||
if (!reactions) {
|
||||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
}
|
||||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onMouseOver = (ev) => {
|
||||
const { key } = ev.target.dataset;
|
||||
const item = this.items.find(({ content }) => content === key);
|
||||
this.setState({
|
||||
hoveredItem: item,
|
||||
});
|
||||
}
|
||||
|
||||
onMouseOut = (ev) => {
|
||||
this.setState({
|
||||
hoveredItem: null,
|
||||
});
|
||||
}
|
||||
|
||||
get items() {
|
||||
return [
|
||||
{
|
||||
content: "👍",
|
||||
title: _t("Agree"),
|
||||
},
|
||||
{
|
||||
content: "👎",
|
||||
title: _t("Disagree"),
|
||||
},
|
||||
{
|
||||
content: "😄",
|
||||
title: _t("Happy"),
|
||||
},
|
||||
{
|
||||
content: "🎉",
|
||||
title: _t("Party Popper"),
|
||||
},
|
||||
{
|
||||
content: "😕",
|
||||
title: _t("Confused"),
|
||||
},
|
||||
{
|
||||
content: "❤️",
|
||||
title: _t("Heart"),
|
||||
},
|
||||
{
|
||||
content: "🚀",
|
||||
title: _t("Rocket"),
|
||||
},
|
||||
{
|
||||
content: "👀",
|
||||
title: _t("Eyes"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mxEvent } = this.props;
|
||||
const { myReactions, hoveredItem } = this.state;
|
||||
const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton');
|
||||
|
||||
const buttons = this.items.map(({ content, title }) => {
|
||||
const myReactionEvent = myReactions && myReactions.find(mxEvent => {
|
||||
if (mxEvent.isRedacted()) {
|
||||
return false;
|
||||
}
|
||||
return mxEvent.getRelation().key === content;
|
||||
});
|
||||
|
||||
return <ReactionTooltipButton
|
||||
key={content}
|
||||
content={content}
|
||||
title={title}
|
||||
mxEvent={mxEvent}
|
||||
myReactionEvent={myReactionEvent}
|
||||
/>;
|
||||
});
|
||||
|
||||
let label = " "; // non-breaking space to keep layout the same when empty
|
||||
if (hoveredItem) {
|
||||
const { content, title } = hoveredItem;
|
||||
|
||||
let shortcodeLabel;
|
||||
const shortcode = unicodeToShortcode(content);
|
||||
if (shortcode) {
|
||||
shortcodeLabel = <span className="mx_ReactionsQuickTooltip_shortcode">
|
||||
{shortcode}
|
||||
</span>;
|
||||
}
|
||||
|
||||
label = <div className="mx_ReactionsQuickTooltip_label">
|
||||
<span className="mx_ReactionsQuickTooltip_title">
|
||||
{title}
|
||||
</span>
|
||||
{shortcodeLabel}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_ReactionsQuickTooltip"
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
>
|
||||
<div className="mx_ReactionsQuickTooltip_buttons">
|
||||
{buttons}
|
||||
</div>
|
||||
{label}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -18,10 +18,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import { isSingleEmoji } from '../../../HtmlUtils';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
// The maximum number of reactions to initially show on a message.
|
||||
const MAX_ITEMS_WHEN_LIMITED = 8;
|
||||
|
||||
export default class ReactionsRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
|
@ -41,6 +45,7 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
|
||||
this.state = {
|
||||
myReactions: this.getMyReactions(),
|
||||
showAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -94,16 +99,22 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onShowAllClick = () => {
|
||||
this.setState({
|
||||
showAll: true,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
const { myReactions } = this.state;
|
||||
const { myReactions, showAll } = this.state;
|
||||
|
||||
if (!reactions || !isContentActionable(mxEvent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||
const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
if (!isSingleEmoji(content)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -125,10 +136,26 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
reactionEvents={events}
|
||||
myReactionEvent={myReactionEvent}
|
||||
/>;
|
||||
});
|
||||
}).filter(item => !!item);
|
||||
|
||||
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
|
||||
// The "+ 1" ensure that the "show all" reveals something that takes up
|
||||
// more space than the button itself.
|
||||
let showAllButton;
|
||||
if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) {
|
||||
items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
|
||||
showAllButton = <a
|
||||
className="mx_ReactionsRow_showAll"
|
||||
href="#"
|
||||
onClick={this.onShowAllClick}
|
||||
>
|
||||
{_t("Show all")}
|
||||
</a>;
|
||||
}
|
||||
|
||||
return <div className="mx_ReactionsRow">
|
||||
{items}
|
||||
{showAllButton}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,12 +30,11 @@ import Modal from '../../../Modal';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {host as matrixtoHost} from '../../../matrix-to';
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TextualBody',
|
||||
|
@ -99,7 +98,7 @@ module.exports = React.createClass({
|
|||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
this.pillifyLinks(this.refs.content.children);
|
||||
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||
HtmlUtils.linkifyElement(this.refs.content);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
|
@ -184,98 +183,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
pillifyLinks: function(nodes) {
|
||||
const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
|
||||
let node = nodes[0];
|
||||
while (node) {
|
||||
let pillified = false;
|
||||
|
||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
||||
const href = node.getAttribute("href");
|
||||
|
||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
if (Pill.isMessagePillUrl(href)) {
|
||||
const pillContainer = document.createElement('span');
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pill = <Pill
|
||||
url={href}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||
/>;
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
node.parentNode.replaceChild(pillContainer, node);
|
||||
// Pills within pills aren't going to go well, so move on
|
||||
pillified = true;
|
||||
|
||||
// update the current node with one that's now taken its place
|
||||
node = pillContainer;
|
||||
}
|
||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
|
||||
let currentTextNode = node;
|
||||
const roomNotifTextNodes = [];
|
||||
|
||||
// Take a textNode and break it up to make all the instances of @room their
|
||||
// own textNode, adding those nodes to roomNotifTextNodes
|
||||
while (currentTextNode !== null) {
|
||||
const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent);
|
||||
let nextTextNode = null;
|
||||
if (roomNotifPos > -1) {
|
||||
let roomTextNode = currentTextNode;
|
||||
|
||||
if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos);
|
||||
if (roomTextNode.textContent.length > Pill.roomNotifLen()) {
|
||||
nextTextNode = roomTextNode.splitText(Pill.roomNotifLen());
|
||||
}
|
||||
roomNotifTextNodes.push(roomTextNode);
|
||||
}
|
||||
currentTextNode = nextTextNode;
|
||||
}
|
||||
|
||||
if (roomNotifTextNodes.length > 0) {
|
||||
const pushProcessor = new PushProcessor(MatrixClientPeg.get());
|
||||
const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif");
|
||||
if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) {
|
||||
// Now replace all those nodes with Pills
|
||||
for (const roomNotifTextNode of roomNotifTextNodes) {
|
||||
// Set the next node to be processed to the one after the node
|
||||
// we're adding now, since we've just inserted nodes into the structure
|
||||
// we're iterating over.
|
||||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
||||
node = roomNotifTextNode.nextSibling;
|
||||
|
||||
const pillContainer = document.createElement('span');
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pill = <Pill
|
||||
type={Pill.TYPE_AT_ROOM_MENTION}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={true}
|
||||
/>;
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
||||
}
|
||||
// Nothing else to do for a text node (and we don't need to advance
|
||||
// the loop pointer because we did it above)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.childNodes && node.childNodes.length && !pillified) {
|
||||
this.pillifyLinks(node.childNodes);
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
},
|
||||
|
||||
findLinks: function(nodes) {
|
||||
let links = [];
|
||||
|
||||
|
@ -448,6 +355,11 @@ module.exports = React.createClass({
|
|||
this.setState({editedMarkerHovered: false});
|
||||
},
|
||||
|
||||
_openHistoryDialog: async function() {
|
||||
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
|
||||
Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
|
||||
},
|
||||
|
||||
_renderEditedMarker: function() {
|
||||
let editedTooltip;
|
||||
if (this.state.editedMarkerHovered) {
|
||||
|
@ -456,12 +368,13 @@ module.exports = React.createClass({
|
|||
const date = editEvent && formatDate(editEvent.getDate());
|
||||
editedTooltip = <Tooltip
|
||||
tooltipClassName="mx_Tooltip_timeline"
|
||||
label={_t("Edited at %(date)s", {date})}
|
||||
label={_t("Edited at %(date)s. Click to view edits.", {date})}
|
||||
/>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key="editedMarker" className="mx_EventTile_edited"
|
||||
onClick={this._openHistoryDialog}
|
||||
onMouseEnter={this._onMouseEnterEditedMarker}
|
||||
onMouseLeave={this._onMouseLeaveEditedMarker}
|
||||
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
||||
|
|
|
@ -24,8 +24,6 @@ import AppTile from '../elements/AppTile';
|
|||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
|
@ -63,20 +61,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
console.log('Failed to connect to integrations server');
|
||||
// TODO -- Handle Scalar errors
|
||||
// this.setState({
|
||||
// scalar_error: err,
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
|
@ -144,16 +128,10 @@ module.exports = React.createClass({
|
|||
|
||||
_launchManageIntegrations: function() {
|
||||
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
||||
this.scalarClient.connect().done(() => {
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
|
||||
null;
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, 'mx_IntegrationsManager');
|
||||
}, (err) => {
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
room: this.props.room,
|
||||
screen: 'add_integ',
|
||||
}, 'mx_IntegrationsManager');
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -403,7 +404,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onCryptoClicked: function(e) {
|
||||
onCryptoClick: function(e) {
|
||||
const event = this.props.mxEvent;
|
||||
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
|
@ -439,7 +440,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
_renderE2EPadlock: function() {
|
||||
const ev = this.props.mxEvent;
|
||||
const props = {onClick: this.onCryptoClicked};
|
||||
const props = {onClick: this.onCryptoClick};
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||
|
@ -670,13 +671,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
|
||||
);
|
||||
|
||||
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
|
||||
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||
const keyRequestInfo = isEncryptionFailure ?
|
||||
<div className="mx_EventTile_keyRequestInfo">
|
||||
<span className="mx_EventTile_keyRequestInfo_text">
|
||||
{ keyRequestInfoContent }
|
||||
</span>
|
||||
<ToolTipButton helpText={keyRequestHelpText} />
|
||||
<TooltipButton helpText={keyRequestHelpText} />
|
||||
</div> : null;
|
||||
|
||||
let reactionsRow;
|
||||
|
@ -828,7 +829,7 @@ module.exports.haveTileForEvent = function(e) {
|
|||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||
|
||||
// No tile for replacement events since they update the original tile
|
||||
if (e.isRelation("m.replace")) return false;
|
||||
if (e.isRelation("m.replace") && SettingsStore.isFeatureEnabled("feature_message_editing")) return false;
|
||||
|
||||
const handler = getHandlerTile(e);
|
||||
if (handler === undefined) return false;
|
||||
|
|
|
@ -60,6 +60,7 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import {ContentHelpers} from 'matrix-js-sdk';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import ComposerHistoryManager from "../../../ComposerHistoryManager";
|
||||
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
|
@ -140,6 +141,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
client: MatrixClient;
|
||||
autocomplete: Autocomplete;
|
||||
historyManager: ComposerHistoryManager;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -329,6 +331,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -679,14 +682,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
if (this.autocomplete.countCompletions() > 0) {
|
||||
if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.LEFT:
|
||||
this.autocomplete.moveSelection(-1);
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
case KeyCode.RIGHT:
|
||||
this.autocomplete.moveSelection(+1);
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
case KeyCode.UP:
|
||||
this.autocomplete.moveSelection(-1);
|
||||
ev.preventDefault();
|
||||
|
@ -1062,6 +1057,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (cmd) {
|
||||
if (!cmd.error) {
|
||||
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||
this.setState({
|
||||
editorState: this.createEditorState(),
|
||||
}, ()=>{
|
||||
|
@ -1139,6 +1135,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||
|
||||
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||
|
||||
if (commandText && commandText.startsWith('/me')) {
|
||||
if (replyingToEv) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -1198,19 +1196,31 @@ export default class MessageComposerInput extends React.Component {
|
|||
};
|
||||
|
||||
onVerticalArrow = (e, up) => {
|
||||
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
|
||||
|
||||
// Select history
|
||||
const selection = this.state.editorState.selection;
|
||||
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
|
||||
|
||||
// selection must be collapsed
|
||||
const selection = this.state.editorState.selection;
|
||||
if (!selection.isCollapsed) return;
|
||||
const document = this.state.editorState.document;
|
||||
|
||||
// and we must be at the edge of the document (up=start, down=end)
|
||||
const document = this.state.editorState.document;
|
||||
if (up) {
|
||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||
} else {
|
||||
if (!selection.anchor.isAtEndOfNode(document)) return;
|
||||
}
|
||||
|
||||
const editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||
const shouldSelectHistory = (editingEnabled && e.altKey) || !editingEnabled;
|
||||
const shouldEditLastMessage = editingEnabled && !e.altKey && up;
|
||||
|
||||
if (shouldSelectHistory) {
|
||||
// Try select composer history
|
||||
const selected = this.selectHistory(up);
|
||||
if (selected) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (shouldEditLastMessage) {
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
|
@ -1223,6 +1233,54 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
selectHistory = (up) => {
|
||||
const delta = up ? -1 : 1;
|
||||
|
||||
// True if we are not currently selecting history, but composing a message
|
||||
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||
// We can't go any further - there isn't any more history, so nop.
|
||||
if (!up) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
currentlyComposedEditorState: this.state.editorState,
|
||||
});
|
||||
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||
// True when we return to the message being composed currently
|
||||
this.setState({
|
||||
editorState: this.state.currentlyComposedEditorState,
|
||||
});
|
||||
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||
return;
|
||||
}
|
||||
|
||||
let editorState;
|
||||
const historyItem = this.historyManager.getItem(delta);
|
||||
if (!historyItem) return;
|
||||
|
||||
if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) {
|
||||
editorState = this.richToMdEditorState(historyItem.value);
|
||||
} else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) {
|
||||
editorState = this.mdToRichEditorState(historyItem.value);
|
||||
} else {
|
||||
editorState = historyItem.value;
|
||||
}
|
||||
|
||||
// Move selection to the end of the selected history
|
||||
const change = editorState.change().moveToEndOfNode(editorState.document);
|
||||
|
||||
// We don't call this.onChange(change) now, as fixups on stuff like pills
|
||||
// should already have been done and persisted in the history.
|
||||
editorState = change.value;
|
||||
|
||||
this.suppressAutoComplete = true;
|
||||
|
||||
this.setState({ editorState }, ()=>{
|
||||
this._editor.focus();
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
onTab = async (e) => {
|
||||
this.setState({
|
||||
someCompletions: null,
|
||||
|
|
|
@ -66,6 +66,7 @@ module.exports = React.createClass({
|
|||
error: PropTypes.object,
|
||||
|
||||
canPreview: PropTypes.bool,
|
||||
previewLoading: PropTypes.bool,
|
||||
room: PropTypes.object,
|
||||
|
||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
||||
|
@ -254,6 +255,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
let showSpinner = false;
|
||||
let darkStyle = false;
|
||||
let title;
|
||||
|
@ -262,6 +265,7 @@ module.exports = React.createClass({
|
|||
let primaryActionLabel;
|
||||
let secondaryActionHandler;
|
||||
let secondaryActionLabel;
|
||||
let footer;
|
||||
|
||||
const messageCase = this._getMessageCase();
|
||||
switch (messageCase) {
|
||||
|
@ -287,6 +291,14 @@ module.exports = React.createClass({
|
|||
primaryActionHandler = this.onRegisterClick;
|
||||
secondaryActionLabel = _t("Sign In");
|
||||
secondaryActionHandler = this.onLoginClick;
|
||||
if (this.props.previewLoading) {
|
||||
footer = (
|
||||
<div>
|
||||
<Spinner w={20} h={20}/>
|
||||
{_t("Loading room preview")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.Kicked: {
|
||||
|
@ -433,7 +445,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
let subTitleElements;
|
||||
if (subTitle) {
|
||||
|
@ -484,6 +495,9 @@ module.exports = React.createClass({
|
|||
{ secondaryButton }
|
||||
{ primaryButton }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -67,14 +68,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_shouldShowNotifBadge: function() {
|
||||
return RoomNotifs.BADGE_STATES.includes(this.state.notifState);
|
||||
},
|
||||
|
||||
_shouldShowMentionBadge: function() {
|
||||
return RoomNotifs.MENTION_BADGE_STATES.includes(this.state.notifState);
|
||||
},
|
||||
|
||||
_isDirectMessageRoom: function(roomId) {
|
||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
return Boolean(dmRooms);
|
||||
|
@ -301,8 +294,8 @@ module.exports = React.createClass({
|
|||
const notificationCount = this.props.notificationCount;
|
||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
||||
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
|
||||
const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState);
|
||||
const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState);
|
||||
const badges = notifBadges || mentionBadges;
|
||||
|
||||
let subtext = null;
|
||||
|
|
|
@ -97,20 +97,22 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="mx_RoomUpgradeWarningBar">
|
||||
<div className="mx_RoomUpgradeWarningBar_header">
|
||||
{_t(
|
||||
"This room is running room version <roomVersion />, which this homeserver has " +
|
||||
"marked as <i>unstable</i>.",
|
||||
{},
|
||||
{
|
||||
"roomVersion": () => <code>{this.props.room.getVersion()}</code>,
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
{doUpgradeWarnings}
|
||||
<div className="mx_RoomUpgradeWarningBar_small">
|
||||
{_t("Only room administrators will see this warning")}
|
||||
<div className="mx_RoomUpgradeWarningBar_wrapped">
|
||||
<div className="mx_RoomUpgradeWarningBar_header">
|
||||
{_t(
|
||||
"This room is running room version <roomVersion />, which this homeserver has " +
|
||||
"marked as <i>unstable</i>.",
|
||||
{},
|
||||
{
|
||||
"roomVersion": () => <code>{this.props.room.getVersion()}</code>,
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
{doUpgradeWarnings}
|
||||
<div className="mx_RoomUpgradeWarningBar_small">
|
||||
{_t("Only room administrators will see this warning")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -53,6 +52,9 @@ export default class Stickerpicker extends React.Component {
|
|||
this.popoverWidth = 300;
|
||||
this.popoverHeight = 300;
|
||||
|
||||
// This is loaded by _acquireScalarClient on an as-needed basis.
|
||||
this.scalarClient = null;
|
||||
|
||||
this.state = {
|
||||
showStickers: false,
|
||||
imError: null,
|
||||
|
@ -63,14 +65,34 @@ export default class Stickerpicker extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
_removeStickerpickerWidgets() {
|
||||
_acquireScalarClient() {
|
||||
if (this.scalarClient) return Promise.resolve(this.scalarClient);
|
||||
if (ScalarAuthClient.isPossible()) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
return this.scalarClient.connect().then(() => {
|
||||
this.forceUpdate();
|
||||
return this.scalarClient;
|
||||
}).catch((e) => {
|
||||
this._imError(_td("Failed to connect to integrations server"), e);
|
||||
});
|
||||
} else {
|
||||
this._imError(_td("No integrations server is configured to manage stickers with"));
|
||||
}
|
||||
}
|
||||
|
||||
async _removeStickerpickerWidgets() {
|
||||
const scalarClient = await this._acquireScalarClient();
|
||||
console.warn('Removing Stickerpicker widgets');
|
||||
if (this.state.widgetId) {
|
||||
this.scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
|
||||
console.warn('Assets disabled');
|
||||
}).catch((err) => {
|
||||
console.error('Failed to disable assets');
|
||||
});
|
||||
if (scalarClient) {
|
||||
scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
|
||||
console.warn('Assets disabled');
|
||||
}).catch((err) => {
|
||||
console.error('Failed to disable assets');
|
||||
});
|
||||
} else {
|
||||
console.error("Cannot disable assets: no scalar client");
|
||||
}
|
||||
} else {
|
||||
console.warn('No widget ID specified, not disabling assets');
|
||||
}
|
||||
|
@ -87,19 +109,7 @@ export default class Stickerpicker extends React.Component {
|
|||
// Close the sticker picker when the window resizes
|
||||
window.addEventListener('resize', this._onResize);
|
||||
|
||||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
this._imError("Failed to connect to integrations server", e);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.state.imError) {
|
||||
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||
}
|
||||
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||
|
||||
// Track updates to widget state in account data
|
||||
MatrixClientPeg.get().on('accountData', this._updateWidget);
|
||||
|
@ -126,7 +136,7 @@ export default class Stickerpicker extends React.Component {
|
|||
console.error(errorMsg, e);
|
||||
this.setState({
|
||||
showStickers: false,
|
||||
imError: errorMsg,
|
||||
imError: _t(errorMsg),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -339,22 +349,13 @@ export default class Stickerpicker extends React.Component {
|
|||
*/
|
||||
_launchManageIntegrations() {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
this.scalarClient.connect().done(() => {
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room,
|
||||
'type_' + widgetType,
|
||||
this.state.widgetId,
|
||||
) :
|
||||
null;
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
this.setState({showStickers: false});
|
||||
}, (err) => {
|
||||
this.setState({imError: err});
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
|
||||
// The integrations manager will handle scalar auth for us.
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
room: this.props.room,
|
||||
screen: `type_${widgetType}`,
|
||||
integrationId: this.state.widgetId,
|
||||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,50 +15,124 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
|
||||
const React = require('react');
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const dis = require('../../../dispatcher');
|
||||
export default class IntegrationsManager extends React.Component {
|
||||
static propTypes = {
|
||||
// the room object where the integrations manager should be opened in
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'IntegrationsManager',
|
||||
// the screen name to open
|
||||
screen: PropTypes.string,
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired, // the source of the integration manager being embedded
|
||||
onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed
|
||||
},
|
||||
// the integration ID to open
|
||||
integrationId: PropTypes.string,
|
||||
|
||||
// XXX: keyboard shortcuts for managing dialogs should be done by the modal
|
||||
// dialog base class somehow, surely...
|
||||
componentDidMount: function() {
|
||||
// callback when the manager is dismissed
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
configured: ScalarAuthClient.isPossible(),
|
||||
connected: false, // true if a `src` is set and able to be connected to
|
||||
src: null, // string for where to connect to
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.state.configured) return;
|
||||
|
||||
const scalarClient = new ScalarAuthClient();
|
||||
scalarClient.connect().then(() => {
|
||||
const hasCredentials = scalarClient.hasCredentials();
|
||||
if (!hasCredentials) {
|
||||
this.setState({
|
||||
connected: false,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
const src = scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room,
|
||||
this.props.screen,
|
||||
this.props.integrationId,
|
||||
);
|
||||
this.setState({
|
||||
loading: false,
|
||||
connected: true,
|
||||
src: src,
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.setState({
|
||||
loading: false,
|
||||
connected: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
}
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
if (ev.keyCode == 27) { // escape
|
||||
onKeyDown = (ev) => {
|
||||
if (ev.keyCode === 27) { // escape
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onAction: function(payload) {
|
||||
onAction = (payload) => {
|
||||
if (payload.action === 'close_scalar') {
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<iframe src={ this.props.src }></iframe>
|
||||
);
|
||||
},
|
||||
});
|
||||
render() {
|
||||
if (!this.state.configured) {
|
||||
return (
|
||||
<div className='mx_IntegrationsManager_error'>
|
||||
<h3>{_t("No integrations server configured")}</h3>
|
||||
<p>{_t("This Riot instance does not have an integrations server configured.")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<div className='mx_IntegrationsManager_loading'>
|
||||
<h3>{_t("Connecting to integrations server...")}</h3>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.connected) {
|
||||
return (
|
||||
<div className='mx_IntegrationsManager_error'>
|
||||
<h3>{_t("Cannot connect to integrations server")}</h3>
|
||||
<p>{_t("The integrations server is offline or it cannot reach your homeserver.")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <iframe src={this.state.src}></iframe>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,48 +29,64 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
super();
|
||||
|
||||
this.state = {
|
||||
mediaDevices: null,
|
||||
mediaDevices: false,
|
||||
activeAudioOutput: null,
|
||||
activeAudioInput: null,
|
||||
activeVideoInput: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
this._refreshMediaDevices();
|
||||
async componentDidMount() {
|
||||
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
|
||||
if (canSeeDeviceLabels) {
|
||||
this._refreshMediaDevices();
|
||||
}
|
||||
}
|
||||
|
||||
_refreshMediaDevices = async (stream) => {
|
||||
if (stream) {
|
||||
// kill stream so that we don't leave it lingering around with webcam enabled etc
|
||||
// as here we called gUM to ask user for permission to their device names only
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
this.setState({
|
||||
mediaDevices: await CallMediaHandler.getDevices(),
|
||||
activeAudioOutput: CallMediaHandler.getAudioOutput(),
|
||||
activeAudioInput: CallMediaHandler.getAudioInput(),
|
||||
activeVideoInput: CallMediaHandler.getVideoInput(),
|
||||
});
|
||||
if (stream) {
|
||||
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
||||
// so that we don't leave it lingering around with webcam enabled etc
|
||||
// as here we called gUM to ask user for permission to their device names only
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
};
|
||||
|
||||
_requestMediaPermissions = () => {
|
||||
const getUserMedia = (
|
||||
window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia
|
||||
);
|
||||
if (getUserMedia) {
|
||||
return getUserMedia.apply(window.navigator, [
|
||||
{ video: true, audio: true },
|
||||
this._refreshMediaDevices,
|
||||
function() {
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
||||
title: _t('No media permissions'),
|
||||
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
|
||||
});
|
||||
},
|
||||
]);
|
||||
_requestMediaPermissions = async () => {
|
||||
let constraints;
|
||||
let stream;
|
||||
let error;
|
||||
try {
|
||||
constraints = {video: true, audio: true};
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
} catch (err) {
|
||||
// user likely doesn't have a webcam,
|
||||
// we should still allow to select a microphone
|
||||
if (err.name === "NotFoundError") {
|
||||
constraints = { audio: true };
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
} else {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
|
||||
title: _t('No media permissions'),
|
||||
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
|
||||
});
|
||||
} else {
|
||||
this._refreshMediaDevices(stream);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -100,7 +116,9 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
};
|
||||
|
||||
_renderDeviceOptions(devices, category) {
|
||||
return devices.map((d) => <option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
|
||||
return devices.map((d) => {
|
||||
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -31,7 +31,7 @@ export default class VerificationCancelled extends React.Component {
|
|||
"The other party cancelled the verification.",
|
||||
)}</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Cancel')}
|
||||
primaryButton={_t('OK')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.props.onDone}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue