Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups
This commit is contained in:
commit
b59b8b7fca
102 changed files with 2211 additions and 1019 deletions
|
@ -746,13 +746,37 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_leaveGroupWarnings: function() {
|
||||
const warnings = [];
|
||||
|
||||
if (this.state.isUserPrivileged) {
|
||||
warnings.push((
|
||||
<span className="warning">
|
||||
{ " " /* Whitespace, otherwise the sentences get smashed together */ }
|
||||
{ _t("You are an administrator of this community. You will not be " +
|
||||
"able to rejoin without an invite from another administrator.") }
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
||||
return warnings;
|
||||
},
|
||||
|
||||
|
||||
_onLeaveClick: function() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const warnings = this._leaveGroupWarnings();
|
||||
|
||||
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
|
||||
title: _t("Leave Community"),
|
||||
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
|
||||
description: (
|
||||
<span>
|
||||
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
|
||||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
button: _t("Leave"),
|
||||
danger: true,
|
||||
danger: this.state.isUserPrivileged,
|
||||
onFinished: async (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
|
||||
|
|
|
@ -181,14 +181,8 @@ var LeftPanel = React.createClass({
|
|||
const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
|
||||
const CallPreview = sdk.getComponent('voip.CallPreview');
|
||||
|
||||
let topBox;
|
||||
if (this.context.matrixClient.isGuest()) {
|
||||
const LoginBox = sdk.getComponent('structures.LoginBox');
|
||||
topBox = <LoginBox collapsed={ this.props.collapsed }/>;
|
||||
} else {
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
|
||||
}
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
|
||||
|
||||
const classes = classNames(
|
||||
"mx_LeftPanel",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 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.
|
||||
|
@ -16,31 +17,15 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import { _t } from '../../languageHandler';
|
||||
var sdk = require('../../index')
|
||||
var dis = require('../../dispatcher');
|
||||
var rate_limited_func = require('../../ratelimitedfunc');
|
||||
var AccessibleButton = require('../../components/views/elements/AccessibleButton');
|
||||
const dis = require('../../dispatcher');
|
||||
const AccessibleButton = require('../../components/views/elements/AccessibleButton');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LoginBox',
|
||||
|
||||
propTypes: {
|
||||
collapsed: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
onToggleCollapse: function(show) {
|
||||
if (show) {
|
||||
dis.dispatch({
|
||||
action: 'show_left_panel',
|
||||
});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({
|
||||
action: 'hide_left_panel',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLoginClick: function() {
|
||||
|
@ -52,41 +37,20 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
|
||||
var toggleCollapse;
|
||||
if (this.props.collapsed) {
|
||||
toggleCollapse =
|
||||
<AccessibleButton className="mx_SearchBox_maximise" onClick={ this.onToggleCollapse.bind(this, true) }>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" alt="Expand panel"/>
|
||||
const loginButton = (
|
||||
<div className="mx_LoginBox_loginButton_wrapper">
|
||||
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
|
||||
{ _t("Login") }
|
||||
</AccessibleButton>
|
||||
}
|
||||
else {
|
||||
toggleCollapse =
|
||||
<AccessibleButton className="mx_SearchBox_minimise" onClick={ this.onToggleCollapse.bind(this, false) }>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="Collapse panel"/>
|
||||
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
|
||||
{ _t("Register") }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
var loginButton;
|
||||
if (!this.props.collapsed) {
|
||||
loginButton = (
|
||||
<div className="mx_LoginBox_loginButton_wrapper">
|
||||
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
|
||||
{ _t("Login") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
|
||||
{ _t("Register") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return (
|
||||
<div className="mx_SearchBox mx_LoginBox">
|
||||
<div className="mx_LoginBox">
|
||||
{ loginButton }
|
||||
{ toggleCollapse }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1034,6 +1034,7 @@ export default React.createClass({
|
|||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
button: _t("Leave"),
|
||||
onFinished: (shouldLeave) => {
|
||||
if (shouldLeave) {
|
||||
const d = MatrixClientPeg.get().leave(roomId);
|
||||
|
@ -1266,6 +1267,9 @@ export default React.createClass({
|
|||
dis.dispatch({action: 'sync_state', prevState, state});
|
||||
|
||||
if (state === "ERROR" || state === "RECONNECTING") {
|
||||
if (data.error instanceof Matrix.InvalidStoreError) {
|
||||
Lifecycle.handleInvalidStoreError(data.error);
|
||||
}
|
||||
self.setState({syncError: data.error || true});
|
||||
} else if (self.state.syncError) {
|
||||
self.setState({syncError: null});
|
||||
|
@ -1401,6 +1405,11 @@ export default React.createClass({
|
|||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
// A later sync can/will correct the tint to be the right value for the user
|
||||
const colorScheme = SettingsStore.getValue("roomColor");
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1743,10 +1752,14 @@ export default React.createClass({
|
|||
}
|
||||
|
||||
if (this.state.view === VIEWS.LOGGED_IN) {
|
||||
// store errors stop the client syncing and require user intervention, so we'll
|
||||
// be showing a dialog. Don't show anything else.
|
||||
const isStoreError = this.state.syncError && this.state.syncError instanceof Matrix.InvalidStoreError;
|
||||
|
||||
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
|
||||
// latter is set via the dispatcher). If we don't yet have a `page_type`,
|
||||
// keep showing the spinner for now.
|
||||
if (this.state.ready && this.state.page_type) {
|
||||
if (this.state.ready && this.state.page_type && !isStoreError) {
|
||||
/* for now, we stuff the entirety of our props and state into the LoggedInView.
|
||||
* we should go through and figure out what we actually need to pass down, as well
|
||||
* as using something like redux to avoid having a billion bits of state kicking around.
|
||||
|
@ -1768,7 +1781,7 @@ export default React.createClass({
|
|||
// we think we are logged in, but are still waiting for the /sync to complete
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
let errorBox;
|
||||
if (this.state.syncError) {
|
||||
if (this.state.syncError && !isStoreError) {
|
||||
errorBox = <div className="mx_MatrixChat_syncError">
|
||||
{messageForSyncError(this.state.syncError)}
|
||||
</div>;
|
||||
|
|
|
@ -542,7 +542,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
// Receipts are objects which have a 'roomMember' and 'ts'.
|
||||
// Receipts are objects which have a 'userId', 'roomMember' and 'ts'.
|
||||
_getReadReceiptsForEvent: function(event) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
|
@ -560,10 +560,8 @@ module.exports = React.createClass({
|
|||
return; // ignore ignored users
|
||||
}
|
||||
const member = room.getMember(r.userId);
|
||||
if (!member) {
|
||||
return; // ignore unknown user IDs
|
||||
}
|
||||
receipts.push({
|
||||
userId: r.userId,
|
||||
roomMember: member,
|
||||
ts: r.data ? r.data.ts : 0,
|
||||
});
|
||||
|
|
|
@ -51,6 +51,7 @@ class HeaderButton extends React.Component {
|
|||
|
||||
return <AccessibleButton
|
||||
aria-label={this.props.title}
|
||||
aria-expanded={this.props.isHighlighted}
|
||||
title={this.props.title}
|
||||
className="mx_RightPanel_headerButton"
|
||||
onClick={this.onClick} >
|
||||
|
@ -345,11 +346,11 @@ module.exports = React.createClass({
|
|||
// being put in the RoomHeader or GroupView header, so only show the minimise
|
||||
// button on these 2 screens or you won't be able to re-expand the panel.
|
||||
headerButtons.push(
|
||||
<div className="mx_RightPanel_headerButton mx_RightPanel_collapsebutton" key="_minimizeButton"
|
||||
<AccessibleButton className="mx_RightPanel_headerButton mx_RightPanel_collapsebutton" key="_minimizeButton"
|
||||
title={_t("Hide panel")} aria-label={_t("Hide panel")} onClick={this.onCollapseClick}
|
||||
>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" />
|
||||
</div>,
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -66,6 +66,10 @@ module.exports = React.createClass({
|
|||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: PropTypes.number,
|
||||
|
||||
// true if the room is being peeked at. This affects components that shouldn't
|
||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||
isPeeking: PropTypes.bool,
|
||||
|
||||
// callback for when the user clicks on the 'resend all' button in the
|
||||
// 'unsent messages' bar
|
||||
onResendAllClick: PropTypes.func,
|
||||
|
@ -223,14 +227,15 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
if (!this.props.atEndOfLiveTimeline) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
<AccessibleButton className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
onClick={this.props.onScrollToBottomClick}>
|
||||
<img src="img/scrolldown.svg" width="24" height="24"
|
||||
alt={_t("Scroll to bottom of page")}
|
||||
title={_t("Scroll to bottom of page")} />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -385,7 +390,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
|
||||
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt="" />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
|
@ -456,7 +461,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// If you're alone in the room, and have sent a message, suggest to invite someone
|
||||
if (this.props.sentMessageAndIsAlone) {
|
||||
if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_isAlone">
|
||||
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
|
||||
|
@ -485,7 +490,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomStatusBar_indicator">
|
||||
{ indicator }
|
||||
</div>
|
||||
{ content }
|
||||
<div role="alert">
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -195,6 +195,8 @@ module.exports = React.createClass({
|
|||
editingRoomSettings: RoomViewStore.isEditingSettings(),
|
||||
};
|
||||
|
||||
if (this.state.editingRoomSettings && !newState.editingRoomSettings) dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||
console.log(
|
||||
'RVS update:',
|
||||
|
@ -676,8 +678,8 @@ module.exports = React.createClass({
|
|||
if (!room) return;
|
||||
|
||||
console.log("Tinter.tint from updateTint");
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
const colorScheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
},
|
||||
|
||||
onAccountData: function(event) {
|
||||
|
@ -692,10 +694,10 @@ module.exports = React.createClass({
|
|||
if (room.roomId == this.state.roomId) {
|
||||
const type = event.getType();
|
||||
if (type === "org.matrix.room.color_scheme") {
|
||||
const color_scheme = event.getContent();
|
||||
const colorScheme = event.getContent();
|
||||
// XXX: we should validate the event
|
||||
console.log("Tinter.tint from onRoomAccountData");
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
|
@ -1512,6 +1514,7 @@ module.exports = React.createClass({
|
|||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
|
@ -1556,6 +1559,7 @@ module.exports = React.createClass({
|
|||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1593,6 +1597,7 @@ module.exports = React.createClass({
|
|||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||
sentMessageAndIsAlone={this.state.isAlone}
|
||||
hasActiveCall={inCall}
|
||||
isPeeking={myMembership !== "join"}
|
||||
onInviteClick={this.onInviteButtonClick}
|
||||
onStopWarningClick={this.onStopAloneWarningClick}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
|
@ -1642,6 +1647,7 @@ module.exports = React.createClass({
|
|||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
canPreview={this.state.canPeek}
|
||||
|
@ -1667,7 +1673,7 @@ module.exports = React.createClass({
|
|||
let messageComposer, searchInfo;
|
||||
const canSpeak = (
|
||||
// joined and not showing search results
|
||||
myMembership == 'join' && !this.state.searchResults
|
||||
myMembership === 'join' && !this.state.searchResults
|
||||
);
|
||||
if (canSpeak) {
|
||||
messageComposer =
|
||||
|
@ -1681,6 +1687,11 @@ module.exports = React.createClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
const LoginBox = sdk.getComponent('structures.LoginBox');
|
||||
messageComposer = <LoginBox/>;
|
||||
}
|
||||
|
||||
// TODO: Why aren't we storing the term/scope/count in this format
|
||||
// in this.state if this is what RoomHeader desires?
|
||||
if (this.state.searchResults) {
|
||||
|
|
|
@ -82,6 +82,7 @@ const SIMPLE_SETTINGS = [
|
|||
{ id: "TagPanel.disableTagPanel" },
|
||||
{ id: "enableWidgetScreenshots" },
|
||||
{ id: "RoomSubList.showEmpty" },
|
||||
{ id: "showDeveloperTools" },
|
||||
];
|
||||
|
||||
// These settings must be defined in SettingsStore
|
||||
|
@ -842,9 +843,9 @@ module.exports = React.createClass({
|
|||
<br />
|
||||
{ _t('Privacy is important to us, so we don\'t collect any personal'
|
||||
+ ' or identifiable data for our analytics.') }
|
||||
<div className="mx_UserSettings_advanced_spoiler" onClick={Analytics.showDetailsModal}>
|
||||
<AccessibleButton className="mx_UserSettings_advanced_spoiler" onClick={Analytics.showDetailsModal}>
|
||||
{ _t('Learn more about how we use analytics.') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -1076,9 +1077,9 @@ module.exports = React.createClass({
|
|||
_renderWebRtcDeviceSettings: function() {
|
||||
if (this.state.mediaDevices === false) {
|
||||
return (
|
||||
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
||||
<AccessibleButton element="p" className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
||||
{ _t('Missing Media Permissions, click here to request.') }
|
||||
</p>
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (!this.state.mediaDevices) return;
|
||||
|
||||
|
@ -1245,7 +1246,7 @@ module.exports = React.createClass({
|
|||
/>
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt={_t("Remove")}
|
||||
<AccessibleButton element="img" src="img/cancel-small.svg" width="14" height="14" alt={_t("Remove")}
|
||||
onClick={onRemoveClick} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1270,7 +1271,7 @@ module.exports = React.createClass({
|
|||
onValueChanged={this._onAddEmailEditFinished} />
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||
<AccessibleButton element="img" src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1339,13 +1340,13 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
|
||||
<div className="mx_UserSettings_avatarPicker">
|
||||
<div className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
|
||||
<AccessibleButton className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
|
||||
<img src="img/cancel.svg"
|
||||
width="15" height="15"
|
||||
className="mx_filterFlipColor"
|
||||
alt={_t("Remove avatar")}
|
||||
title={_t("Remove avatar")} />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
<div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer">
|
||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
|
||||
|
@ -1406,11 +1407,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t('Access Token:') + ' ' }
|
||||
<span className="mx_UserSettings_advanced_spoiler"
|
||||
<AccessibleButton element="span" className="mx_UserSettings_advanced_spoiler"
|
||||
onClick={this._showSpoiler}
|
||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
<{ _t("click to reveal") }>
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() }
|
||||
|
|
|
@ -26,7 +26,8 @@ module.exports = React.createClass({
|
|||
displayName: 'MemberAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: PropTypes.object.isRequired,
|
||||
member: PropTypes.object,
|
||||
fallbackUserId: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
|
@ -55,23 +56,30 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_getState: function(props) {
|
||||
if (!props.member) {
|
||||
console.error("MemberAvatar called somehow with null member");
|
||||
if (props.member) {
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod),
|
||||
};
|
||||
} else if (props.fallbackUserId) {
|
||||
return {
|
||||
name: props.fallbackUserId,
|
||||
title: props.fallbackUserId,
|
||||
};
|
||||
} else {
|
||||
console.error("MemberAvatar called somehow with null member or fallbackUserId");
|
||||
}
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod),
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
let {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
let userId = member ? member.userId : fallbackUserId;
|
||||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
|
@ -84,7 +92,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -625,7 +625,7 @@ export default class DevtoolsDialog extends React.Component {
|
|||
let body;
|
||||
|
||||
if (this.state.mode) {
|
||||
body = <div>
|
||||
body = <div className="mx_DevTools_dialog">
|
||||
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
|
@ -634,7 +634,7 @@ export default class DevtoolsDialog extends React.Component {
|
|||
} else {
|
||||
const classes = "mx_DevTools_RoomStateExplorer_button";
|
||||
body = <div>
|
||||
<div>
|
||||
<div className="mx_DevTools_dialog">
|
||||
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
|
|
39
src/components/views/dialogs/LazyLoadingDisabledDialog.js
Normal file
39
src/components/views/dialogs/LazyLoadingDisabledDialog.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2018 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 QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default (props) => {
|
||||
const description1 =
|
||||
_t("You've previously used Riot on %(host)s with lazy loading of members enabled. " +
|
||||
"In this version lazy loading is disabled. " +
|
||||
"As the local cache is not compatible between these two settings, " +
|
||||
"Riot needs to resync your account.",
|
||||
{host: props.host});
|
||||
const description2 = _t("If the other version of Riot is still open in another tab, " +
|
||||
"please close it as using Riot on the same host with both " +
|
||||
"lazy loading enabled and disabled simultaneously will cause issues.");
|
||||
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
title={_t("Incompatible local cache")}
|
||||
description={<div><p>{description1}</p><p>{description2}</p></div>}
|
||||
button={_t("Clear cache and resync")}
|
||||
onFinished={props.onFinished}
|
||||
/>);
|
||||
};
|
|
@ -20,7 +20,8 @@ import { _t } from '../../../languageHandler';
|
|||
|
||||
export default (props) => {
|
||||
const description =
|
||||
_t("Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!");
|
||||
_t("Riot now uses 3-5x less memory, by only loading information about other users"
|
||||
+ " when needed. Please wait whilst we resynchronise with the server!");
|
||||
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
|
|
|
@ -62,6 +62,7 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description }
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const AppWarning = (props) => {
|
||||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||
<img src='img/warning.svg' alt='' />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
||||
|
|
|
@ -74,6 +74,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
return (
|
||||
<div className="mx_Dialog_buttons">
|
||||
{ cancelButton }
|
||||
{ this.props.children }
|
||||
<button className={primaryButtonClassName}
|
||||
onClick={this.props.onPrimaryButtonClick}
|
||||
autoFocus={this.props.focus}
|
||||
|
@ -81,8 +83,6 @@ module.exports = React.createClass({
|
|||
>
|
||||
{ this.props.primaryButton }
|
||||
</button>
|
||||
{ this.props.children }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -51,7 +51,7 @@ export default class CookieBar extends React.Component {
|
|||
const toolbarClasses = "mx_MatrixToolbar";
|
||||
return (
|
||||
<div className={toolbarClasses}>
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning" />
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ this.props.policyUrl ? _t(
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
|
@ -95,7 +95,7 @@ export default class CookieBar extends React.Component {
|
|||
{ _t("Yes, I want to help!") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
|
||||
<img src="img/cancel.svg" width="18" height="18" />
|
||||
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -35,11 +35,11 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning"/>
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
|
||||
</div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/></AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -96,7 +96,7 @@ export default React.createClass({
|
|||
}
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning"/>
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{_t("A new version of Riot is available.")}
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,7 @@ export default React.createClass({
|
|||
src="img/warning.svg"
|
||||
width="24"
|
||||
height="23"
|
||||
alt="Warning"
|
||||
alt=""
|
||||
/>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t(
|
||||
|
|
|
@ -71,9 +71,9 @@ export default React.createClass({
|
|||
|
||||
let image;
|
||||
if (doneStatuses.includes(this.props.status)) {
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt={warning}/>;
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="" />;
|
||||
} else {
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/spinner.gif" width="24" height="23" alt={message}/>;
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/spinner.gif" width="24" height="23" alt="" />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -83,7 +83,7 @@ export default React.createClass({
|
|||
{message}
|
||||
</div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.hideToolbar}>
|
||||
<img src="img/cancel.svg" width="18" height="18" />
|
||||
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -165,7 +165,7 @@ export default React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_outerWrapper">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
{ joined }
|
||||
{ invited }
|
||||
</GeminiScrollbarWrapper>
|
||||
|
|
|
@ -22,6 +22,7 @@ import classnames from 'classnames';
|
|||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -209,6 +210,125 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export const TermsAuthEntry = React.createClass({
|
||||
displayName: 'TermsAuthEntry',
|
||||
|
||||
statics: {
|
||||
LOGIN_TYPE: "m.login.terms",
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// example stageParams:
|
||||
//
|
||||
// {
|
||||
// "policies": {
|
||||
// "privacy_policy": {
|
||||
// "version": "1.0",
|
||||
// "en": {
|
||||
// "name": "Privacy Policy",
|
||||
// "url": "https://example.org/privacy-1.0-en.html",
|
||||
// },
|
||||
// "fr": {
|
||||
// "name": "Politique de confidentialité",
|
||||
// "url": "https://example.org/privacy-1.0-fr.html",
|
||||
// },
|
||||
// },
|
||||
// "other_policy": { ... },
|
||||
// }
|
||||
// }
|
||||
|
||||
const allPolicies = this.props.stageParams.policies || {};
|
||||
const prefLang = SettingsStore.getValue("language");
|
||||
const initToggles = {};
|
||||
const pickedPolicies = [];
|
||||
for (const policyId of Object.keys(allPolicies)) {
|
||||
const policy = allPolicies[policyId];
|
||||
|
||||
// Pick a language based on the user's language, falling back to english,
|
||||
// and finally to the first language available. If there's still no policy
|
||||
// available then the homeserver isn't respecting the spec.
|
||||
let langPolicy = policy[prefLang];
|
||||
if (!langPolicy) langPolicy = policy["en"];
|
||||
if (!langPolicy) {
|
||||
// last resort
|
||||
const firstLang = Object.keys(policy).find(e => e !== "version");
|
||||
langPolicy = policy[firstLang];
|
||||
}
|
||||
if (!langPolicy) throw new Error("Failed to find a policy to show the user");
|
||||
|
||||
initToggles[policyId] = false;
|
||||
|
||||
langPolicy.id = policyId;
|
||||
pickedPolicies.push(langPolicy);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
"toggledPolicies": initToggles,
|
||||
"policies": pickedPolicies,
|
||||
});
|
||||
},
|
||||
|
||||
_trySubmit: function(policyId) {
|
||||
const newToggles = {};
|
||||
let allChecked = true;
|
||||
for (const policy of this.state.policies) {
|
||||
let checked = this.state.toggledPolicies[policy.id];
|
||||
if (policy.id === policyId) checked = !checked;
|
||||
|
||||
newToggles[policy.id] = checked;
|
||||
allChecked = allChecked && checked;
|
||||
}
|
||||
|
||||
this.setState({"toggledPolicies": newToggles});
|
||||
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.props.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
let checkboxes = [];
|
||||
let allChecked = true;
|
||||
for (const policy of this.state.policies) {
|
||||
const checked = this.state.toggledPolicies[policy.id];
|
||||
allChecked = allChecked && checked;
|
||||
|
||||
checkboxes.push(
|
||||
<label key={"policy_checkbox_" + policy.id}>
|
||||
<input type="checkbox" onClick={() => this._trySubmit(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
|
||||
{ checkboxes }
|
||||
{ errorSection }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const EmailIdentityAuthEntry = React.createClass({
|
||||
displayName: 'EmailIdentityAuthEntry',
|
||||
|
||||
|
@ -496,6 +616,7 @@ const AuthEntryComponents = [
|
|||
RecaptchaAuthEntry,
|
||||
EmailIdentityAuthEntry,
|
||||
MsisdnAuthEntry,
|
||||
TermsAuthEntry,
|
||||
];
|
||||
|
||||
export function getEntryComponentForLoginType(loginType) {
|
||||
|
|
|
@ -96,7 +96,7 @@ export default React.createClass({
|
|||
render() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = this.props;
|
||||
let name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
|
|
|
@ -220,8 +220,9 @@ module.exports = React.createClass({
|
|||
let canonical_alias_section;
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
canonical_alias_section = (
|
||||
<select onChange={this.onCanonicalAliasChange} value={this.state.canonicalAlias}>
|
||||
<select onChange={this.onCanonicalAliasChange} value={canonicalValue}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(self.state.domainToAliases).map((domain, i) => {
|
||||
|
|
|
@ -30,6 +30,7 @@ import ScalarMessaging from '../../../ScalarMessaging';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
|
@ -193,17 +194,15 @@ module.exports = React.createClass({
|
|||
if (this.props.showApps &&
|
||||
this._canUserModify()
|
||||
) {
|
||||
addWidget = <div
|
||||
addWidget = <AccessibleButton
|
||||
onClick={this.onClickAddWidget}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
className={this.state.apps.length<2 ?
|
||||
'mx_AddWidget_button mx_AddWidget_button_full_width' :
|
||||
'mx_AddWidget_button'
|
||||
}
|
||||
title={_t('Add a widget')}>
|
||||
[+] { _t('Add a widget') }
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let spinner;
|
||||
|
|
|
@ -114,7 +114,7 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
processQuery(query, selection) {
|
||||
return this.autocompleter.getCompletions(
|
||||
query, selection, this.state.forceComplete
|
||||
query, selection, this.state.forceComplete,
|
||||
).then((completions) => {
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
if (query !== this.queryRequested) {
|
||||
|
|
|
@ -277,7 +277,11 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return false;
|
||||
}
|
||||
for (let j = 0; j < rA.length; j++) {
|
||||
if (rA[j].roomMember.userId !== rB[j].roomMember.userId) {
|
||||
if (rA[j].userId !== rB[j].userId) {
|
||||
return false;
|
||||
}
|
||||
// one has a member set and the other doesn't?
|
||||
if (rA[j].roomMember !== rB[j].roomMember) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -359,7 +363,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// else set it proportional to index
|
||||
left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset;
|
||||
|
||||
const userId = receipt.roomMember.userId;
|
||||
const userId = receipt.userId;
|
||||
let readReceiptInfo;
|
||||
|
||||
if (this.props.readReceiptMap) {
|
||||
|
@ -373,6 +377,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
||||
avatars.unshift(
|
||||
<ReadReceiptMarker key={userId} member={receipt.roomMember}
|
||||
fallbackUserId={userId}
|
||||
leftOffset={left} hidden={hidden}
|
||||
readReceiptInfo={readReceiptInfo}
|
||||
checkUnmounting={this.props.checkUnmounting}
|
||||
|
|
|
@ -777,7 +777,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const myMembership = room.getMyMembership();
|
||||
// not a DM room if we have are not joined
|
||||
if (myMembership !== 'join') continue;
|
||||
|
||||
|
||||
const them = this.props.member;
|
||||
// not a DM room if they are not joined
|
||||
if (!them.membership || them.membership !== 'join') continue;
|
||||
|
@ -935,7 +935,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
|
|
|
@ -447,7 +447,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_joined">
|
||||
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
|
|
|
@ -292,21 +292,22 @@ export default class MessageComposer extends React.Component {
|
|||
let videoCallButton;
|
||||
let hangupButton;
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
// Call buttons
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
hangupButton =
|
||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
<AccessibleButton key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
<img src="img/hangup.svg" alt={_t('Hangup')} title={_t('Hangup')} width="25" height="26" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
callButton =
|
||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
|
||||
<AccessibleButton key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
|
||||
<TintableSvg src="img/icon-call.svg" width="35" height="35" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
videoCallButton =
|
||||
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
|
||||
<AccessibleButton key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
|
||||
<TintableSvg src="img/icons-video.svg" width="35" height="35" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const canSendMessages = !this.state.tombstone &&
|
||||
|
@ -317,18 +318,19 @@ export default class MessageComposer extends React.Component {
|
|||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
const uploadButton = (
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
<AccessibleButton key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title={_t('Upload file')}>
|
||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35" />
|
||||
<input ref="uploadInput" type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onChange={this.onUploadFileSelected} />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
const formattingButton = this.state.inputState.isRichTextEnabled ? (
|
||||
<img className="mx_MessageComposer_formatting"
|
||||
<AccessibleButton element="img" className="mx_MessageComposer_formatting"
|
||||
alt={_t("Show Text Formatting Toolbar")}
|
||||
title={_t("Show Text Formatting Toolbar")}
|
||||
src="img/button-text-formatting.svg"
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
|
@ -372,7 +374,6 @@ export default class MessageComposer extends React.Component {
|
|||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<img className="mx_MessageComposer_roomReplaced_icon" src="img/room_replaced.svg" />
|
||||
|
@ -423,7 +424,7 @@ export default class MessageComposer extends React.Component {
|
|||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={`img/button-md-${!this.state.inputState.isRichTextEnabled}.png`} />
|
||||
<img title={_t("Hide Text Formatting Toolbar")}
|
||||
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
|
|
|
@ -106,6 +106,17 @@ const MARK_TAGS = {
|
|||
s: 'deleted', // deprecated
|
||||
};
|
||||
|
||||
const SLATE_SCHEMA = {
|
||||
inlines: {
|
||||
pill: {
|
||||
isVoid: true,
|
||||
},
|
||||
emoji: {
|
||||
isVoid: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function onSendMessageFailed(err, room) {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
|
@ -116,10 +127,10 @@ function onSendMessageFailed(err, room) {
|
|||
}
|
||||
|
||||
function rangeEquals(a: Range, b: Range): boolean {
|
||||
return (a.anchorKey === b.anchorKey
|
||||
&& a.anchorOffset === b.anchorOffset
|
||||
&& a.focusKey === b.focusKey
|
||||
&& a.focusOffset === b.focusOffset
|
||||
return (a.anchor.key === b.anchor.key
|
||||
&& a.anchor.offset === b.anchorOffset
|
||||
&& a.focus.key === b.focusKey
|
||||
&& a.focus.offset === b.focusOffset
|
||||
&& a.isFocused === b.isFocused
|
||||
&& a.isBackward === b.isBackward);
|
||||
}
|
||||
|
@ -213,7 +224,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
object: 'block',
|
||||
type: type,
|
||||
nodes: next(el.childNodes),
|
||||
}
|
||||
};
|
||||
}
|
||||
type = MARK_TAGS[tag];
|
||||
if (type) {
|
||||
|
@ -221,7 +232,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
object: 'mark',
|
||||
type: type,
|
||||
nodes: next(el.childNodes),
|
||||
}
|
||||
};
|
||||
}
|
||||
// special case links
|
||||
if (tag === 'a') {
|
||||
|
@ -239,16 +250,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
completion: el.innerText,
|
||||
completionId: m[1],
|
||||
},
|
||||
isVoid: true,
|
||||
}
|
||||
}
|
||||
else {
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
object: 'inline',
|
||||
type: 'link',
|
||||
data: { href },
|
||||
nodes: next(el.childNodes),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -258,14 +267,12 @@ export default class MessageComposerInput extends React.Component {
|
|||
node: obj,
|
||||
children: children,
|
||||
});
|
||||
}
|
||||
else if (obj.object === 'mark') {
|
||||
} else if (obj.object === 'mark') {
|
||||
return this.renderMark({
|
||||
mark: obj,
|
||||
children: children,
|
||||
});
|
||||
}
|
||||
else if (obj.object === 'inline') {
|
||||
} else if (obj.object === 'inline') {
|
||||
// special case links, pills and emoji otherwise we
|
||||
// end up with React components getting rendered out(!)
|
||||
switch (obj.type) {
|
||||
|
@ -285,9 +292,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
children: children,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const savedState = MessageComposerStore.getEditorState(this.props.room.roomId);
|
||||
|
@ -345,9 +352,13 @@ export default class MessageComposerInput extends React.Component {
|
|||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_collectEditor = (e) => {
|
||||
this._editor = e;
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
const editor = this.refs.editor;
|
||||
let editorState = this.state.editorState;
|
||||
const editor = this._editor;
|
||||
const editorState = this.state.editorState;
|
||||
|
||||
switch (payload.action) {
|
||||
case 'reply_to_event':
|
||||
|
@ -402,20 +413,24 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
// XXX: this is to bring back the focus in a sane place and add a paragraph after it
|
||||
change = change.select({
|
||||
anchorKey: quote.key,
|
||||
focusKey: quote.key,
|
||||
}).collapseToEndOfBlock().insertBlock(Block.create(DEFAULT_NODE)).focus();
|
||||
change = change.select(Range.create({
|
||||
anchor: {
|
||||
key: quote.key,
|
||||
},
|
||||
focus: {
|
||||
key: quote.key,
|
||||
},
|
||||
})).moveToEndOfBlock().insertBlock(Block.create(DEFAULT_NODE)).focus();
|
||||
|
||||
this.onChange(change);
|
||||
} else {
|
||||
let fragmentChange = fragment.change();
|
||||
fragmentChange.moveToRangeOf(fragment.document)
|
||||
const fragmentChange = fragment.change();
|
||||
fragmentChange.moveToRangeOfNode(fragment.document)
|
||||
.wrapBlock(quote);
|
||||
|
||||
// FIXME: handle pills and use commonmark rather than md-serialize
|
||||
const md = this.md.serialize(fragmentChange.value);
|
||||
let change = editorState.change()
|
||||
const change = editorState.change()
|
||||
.insertText(md + '\n\n')
|
||||
.focus();
|
||||
this.onChange(change);
|
||||
|
@ -497,15 +512,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (this.direction !== '') {
|
||||
const focusedNode = editorState.focusInline || editorState.focusText;
|
||||
if (focusedNode.isVoid) {
|
||||
if (editorState.schema.isVoid(focusedNode)) {
|
||||
// XXX: does this work in RTL?
|
||||
const edge = this.direction === 'Previous' ? 'End' : 'Start';
|
||||
if (editorState.isCollapsed) {
|
||||
change = change[`collapseTo${ edge }Of${ this.direction }Text`]();
|
||||
if (editorState.selection.isCollapsed) {
|
||||
change = change[`moveTo${ edge }Of${ this.direction }Text`]();
|
||||
} else {
|
||||
const block = this.direction === 'Previous' ? editorState.previousText : editorState.nextText;
|
||||
if (block) {
|
||||
change = change[`moveFocusTo${ edge }Of`](block);
|
||||
change = change[`moveFocusTo${ edge }OfNode`](block);
|
||||
}
|
||||
}
|
||||
editorState = change.value;
|
||||
|
@ -517,12 +532,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
if (this.autocomplete.state.completionList.length > 0 && !this.autocomplete.state.hide &&
|
||||
!rangeEquals(this.state.editorState.selection, editorState.selection) &&
|
||||
// XXX: the heuristic failed when inlines like pills weren't taken into account. This is inideal
|
||||
this.state.editorState.document.toJSON() === editorState.document.toJSON())
|
||||
{
|
||||
this.state.editorState.document.toJSON() === editorState.document.toJSON()) {
|
||||
this.autocomplete.hide();
|
||||
}
|
||||
|
||||
if (!editorState.document.isEmpty) {
|
||||
if (Plain.serialize(editorState) !== '') {
|
||||
this.onTypingActivity();
|
||||
} else {
|
||||
this.onFinishedTyping();
|
||||
|
@ -543,10 +557,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
|
||||
|
||||
const range = Range.create({
|
||||
anchorKey: editorState.selection.startKey,
|
||||
anchorOffset: currentStartOffset - emojiMatch[1].length - 1,
|
||||
focusKey: editorState.selection.startKey,
|
||||
focusOffset: currentStartOffset - 1,
|
||||
anchor: {
|
||||
key: editorState.selection.startKey,
|
||||
offset: currentStartOffset - emojiMatch[1].length - 1,
|
||||
},
|
||||
focus: {
|
||||
key: editorState.selection.startKey,
|
||||
offset: currentStartOffset - 1,
|
||||
},
|
||||
});
|
||||
change = change.insertTextAtRange(range, unicodeEmoji);
|
||||
editorState = change.value;
|
||||
|
@ -555,35 +573,50 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
// emojioneify any emoji
|
||||
editorState.document.getTexts().forEach(node => {
|
||||
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
|
||||
let match;
|
||||
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
|
||||
const range = Range.create({
|
||||
anchorKey: node.key,
|
||||
anchorOffset: match.index,
|
||||
focusKey: node.key,
|
||||
focusOffset: match.index + match[0].length,
|
||||
});
|
||||
const inline = Inline.create({
|
||||
type: 'emoji',
|
||||
data: { emojiUnicode: match[0] },
|
||||
isVoid: true,
|
||||
});
|
||||
change = change.insertInlineAtRange(range, inline);
|
||||
editorState = change.value;
|
||||
let foundEmoji;
|
||||
do {
|
||||
foundEmoji = false;
|
||||
|
||||
for (const node of editorState.document.getTexts()) {
|
||||
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
|
||||
let match;
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: node.key,
|
||||
offset: match.index,
|
||||
},
|
||||
focus: {
|
||||
key: node.key,
|
||||
offset: match.index + match[0].length,
|
||||
},
|
||||
});
|
||||
const inline = Inline.create({
|
||||
type: 'emoji',
|
||||
data: { emojiUnicode: match[0] },
|
||||
});
|
||||
change = change.insertInlineAtRange(range, inline);
|
||||
editorState = change.value;
|
||||
|
||||
// if we replaced an emoji, start again looking for more
|
||||
// emoji in the new editor state since doing the replacement
|
||||
// will change the node structure & offsets so we can't compute
|
||||
// insertion ranges from node.key / match.index anymore.
|
||||
foundEmoji = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} while (foundEmoji);
|
||||
|
||||
// work around weird bug where inserting emoji via the macOS
|
||||
// emoji picker can leave the selection stuck in the emoji's
|
||||
// child text. This seems to happen due to selection getting
|
||||
// moved in the normalisation phase after calculating these changes
|
||||
if (editorState.anchorKey &&
|
||||
editorState.document.getParent(editorState.anchorKey).type === 'emoji')
|
||||
{
|
||||
change = change.collapseToStartOfNextText();
|
||||
if (editorState.selection.anchor.key &&
|
||||
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
|
||||
change = change.moveToStartOfNextText();
|
||||
editorState = change.value;
|
||||
}
|
||||
|
||||
|
@ -595,15 +628,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
const parent = editorState.document.getParent(editorState.blocks.first().key);
|
||||
if (parent.type === 'numbered-list') {
|
||||
blockType = 'numbered-list';
|
||||
}
|
||||
else if (parent.type === 'bulleted-list') {
|
||||
} else if (parent.type === 'bulleted-list') {
|
||||
blockType = 'bulleted-list';
|
||||
}
|
||||
}
|
||||
const inputState = {
|
||||
marks: editorState.activeMarks,
|
||||
isRichTextEnabled: this.state.isRichTextEnabled,
|
||||
blockType
|
||||
blockType,
|
||||
};
|
||||
this.props.onInputStateChanged(inputState);
|
||||
}
|
||||
|
@ -613,7 +645,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
this.setState({
|
||||
editorState,
|
||||
originalEditorState: originalEditorState || null
|
||||
originalEditorState: originalEditorState || null,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -653,7 +685,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
// which doesn't roundtrip symmetrically with commonmark, which we use for
|
||||
// compiling MD out of the MD editor state above.
|
||||
this.md.serialize(editorState),
|
||||
{ defaultBlock: DEFAULT_NODE }
|
||||
{ defaultBlock: DEFAULT_NODE },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -673,11 +705,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
editorState: this.createEditorState(enabled, editorState),
|
||||
isRichTextEnabled: enabled,
|
||||
}, ()=>{
|
||||
this.refs.editor.focus();
|
||||
this._editor.focus();
|
||||
});
|
||||
|
||||
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current selection has a mark with `type` in it.
|
||||
|
@ -687,8 +719,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
*/
|
||||
|
||||
hasMark = type => {
|
||||
const { editorState } = this.state
|
||||
return editorState.activeMarks.some(mark => mark.type === type)
|
||||
const { editorState } = this.state;
|
||||
return editorState.activeMarks.some(mark => mark.type === type);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -699,20 +731,18 @@ export default class MessageComposerInput extends React.Component {
|
|||
*/
|
||||
|
||||
hasBlock = type => {
|
||||
const { editorState } = this.state
|
||||
return editorState.blocks.some(node => node.type === type)
|
||||
const { editorState } = this.state;
|
||||
return editorState.blocks.some(node => node.type === type);
|
||||
};
|
||||
|
||||
onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => {
|
||||
|
||||
this.suppressAutoComplete = false;
|
||||
|
||||
// skip void nodes - see
|
||||
// https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
|
||||
if (ev.keyCode === KeyCode.LEFT) {
|
||||
this.direction = 'Previous';
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.RIGHT) {
|
||||
} else if (ev.keyCode === KeyCode.RIGHT) {
|
||||
this.direction = 'Next';
|
||||
} else {
|
||||
this.direction = '';
|
||||
|
@ -760,7 +790,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
// drop a point in history so the user can undo a word
|
||||
// XXX: this seems nasty but adding to history manually seems a no-go
|
||||
ev.preventDefault();
|
||||
return change.setOperationFlag("skip", false).setOperationFlag("merge", false).insertText(ev.key);
|
||||
return change.withoutMerging(() => {
|
||||
change.insertText(ev.key);
|
||||
});
|
||||
};
|
||||
|
||||
onBackspace = (ev: KeyboardEvent, change: Change): Change => {
|
||||
|
@ -771,23 +803,24 @@ export default class MessageComposerInput extends React.Component {
|
|||
const { editorState } = this.state;
|
||||
|
||||
// Allow Ctrl/Cmd-Backspace when focus starts at the start of the composer (e.g select-all)
|
||||
// for some reason if slate sees you Ctrl-backspace and your anchorOffset=0 it just resets your focus
|
||||
if (!editorState.isCollapsed && editorState.anchorOffset === 0) {
|
||||
// for some reason if slate sees you Ctrl-backspace and your anchor.offset=0 it just resets your focus
|
||||
// XXX: Doing this now seems to put slate into a broken state, and it didn't appear to be doing
|
||||
// what it claims to do on the old version of slate anyway...
|
||||
/*if (!editorState.isCollapsed && editorState.selection.anchor.offset === 0) {
|
||||
return change.delete();
|
||||
}
|
||||
}*/
|
||||
|
||||
if (this.state.isRichTextEnabled) {
|
||||
// let backspace exit lists
|
||||
const isList = this.hasBlock('list-item');
|
||||
|
||||
if (isList && editorState.anchorOffset == 0) {
|
||||
if (isList && editorState.selection.anchor.offset == 0) {
|
||||
change
|
||||
.setBlocks(DEFAULT_NODE)
|
||||
.unwrapBlock('bulleted-list')
|
||||
.unwrapBlock('numbered-list');
|
||||
return change;
|
||||
}
|
||||
else if (editorState.anchorOffset == 0 && editorState.isCollapsed) {
|
||||
} else if (editorState.selection.anchor.offset == 0 && editorState.isCollapsed) {
|
||||
// turn blocks back into paragraphs
|
||||
if ((this.hasBlock('block-quote') ||
|
||||
this.hasBlock('heading1') ||
|
||||
|
@ -796,20 +829,18 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.hasBlock('heading4') ||
|
||||
this.hasBlock('heading5') ||
|
||||
this.hasBlock('heading6') ||
|
||||
this.hasBlock('code')))
|
||||
{
|
||||
this.hasBlock('code'))) {
|
||||
return change.setBlocks(DEFAULT_NODE);
|
||||
}
|
||||
|
||||
// remove paragraphs entirely if they're nested
|
||||
const parent = editorState.document.getParent(editorState.anchorBlock.key);
|
||||
if (editorState.anchorOffset == 0 &&
|
||||
if (editorState.selection.anchor.offset == 0 &&
|
||||
this.hasBlock('paragraph') &&
|
||||
parent.nodes.size == 1 &&
|
||||
parent.object !== 'document')
|
||||
{
|
||||
parent.object !== 'document') {
|
||||
return change.replaceNodeByKey(editorState.anchorBlock.key, editorState.anchorText)
|
||||
.collapseToEndOf(parent)
|
||||
.moveToEndOfNode(parent)
|
||||
.focus();
|
||||
}
|
||||
}
|
||||
|
@ -823,7 +854,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
let newState: ?Value = null;
|
||||
const newState: ?Value = null;
|
||||
|
||||
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
|
||||
if (this.state.isRichTextEnabled) {
|
||||
|
@ -849,7 +880,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
} else if (isList) {
|
||||
change
|
||||
.unwrapBlock(
|
||||
type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list'
|
||||
type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list',
|
||||
)
|
||||
.wrapBlock(type);
|
||||
} else {
|
||||
|
@ -942,8 +973,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
const collapseAndOffsetSelection = (selection, offset) => {
|
||||
const key = selection.endKey();
|
||||
return new Range({
|
||||
anchorKey: key, anchorOffset: offset,
|
||||
focusKey: key, focusOffset: offset,
|
||||
anchorKey: key, anchor.offset: offset,
|
||||
focus.key: key, focus.offset: offset,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1000,18 +1031,16 @@ export default class MessageComposerInput extends React.Component {
|
|||
.insertFragment(fragment.document);
|
||||
} else {
|
||||
// in MD mode we don't want the rich content pasted as the magic was annoying people so paste plain
|
||||
return change
|
||||
.setOperationFlag("skip", false)
|
||||
.setOperationFlag("merge", false)
|
||||
.insertText(transfer.text);
|
||||
return change.withoutMerging(() => {
|
||||
change.insertText(transfer.text);
|
||||
});
|
||||
}
|
||||
}
|
||||
case 'text':
|
||||
// don't skip/merge so that multiple consecutive pastes can be undone individually
|
||||
return change
|
||||
.setOperationFlag("skip", false)
|
||||
.setOperationFlag("merge", false)
|
||||
.insertText(transfer.text);
|
||||
return change.withoutMerging(() => {
|
||||
change.insertText(transfer.text);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1054,8 +1083,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
const firstGrandChild = firstChild && firstChild.nodes.get(0);
|
||||
if (firstChild && firstGrandChild &&
|
||||
firstChild.object === 'block' && firstGrandChild.object === 'text' &&
|
||||
firstGrandChild.text[0] === '/')
|
||||
{
|
||||
firstGrandChild.text[0] === '/') {
|
||||
commandText = this.plainWithIdPills.serialize(editorState);
|
||||
cmd = processCommandInput(this.props.room.roomId, commandText);
|
||||
}
|
||||
|
@ -1066,7 +1094,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.setState({
|
||||
editorState: this.createEditorState(),
|
||||
}, ()=>{
|
||||
this.refs.editor.focus();
|
||||
this._editor.focus();
|
||||
});
|
||||
}
|
||||
if (cmd.promise) {
|
||||
|
@ -1196,7 +1224,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
this.setState({
|
||||
editorState: this.createEditorState(),
|
||||
}, ()=>{ this.refs.editor.focus() });
|
||||
}, ()=>{ this._editor.focus(); });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -1216,9 +1244,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
// and we must be at the edge of the document (up=start, down=end)
|
||||
if (up) {
|
||||
if (!selection.isAtStartOf(document)) return;
|
||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||
} else {
|
||||
if (!selection.isAtEndOf(document)) return;
|
||||
if (!selection.anchor.isAtEndOfNode(document)) return;
|
||||
}
|
||||
|
||||
const selected = this.selectHistory(up);
|
||||
|
@ -1266,7 +1294,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
// Move selection to the end of the selected history
|
||||
const change = editorState.change().collapseToEndOf(editorState.document);
|
||||
const change = editorState.change().moveToEndOfNode(editorState.document);
|
||||
|
||||
// We don't call this.onChange(change) now, as fixups on stuff like emoji
|
||||
// should already have been done and persisted in the history.
|
||||
|
@ -1275,7 +1303,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.suppressAutoComplete = true;
|
||||
|
||||
this.setState({ editorState }, ()=>{
|
||||
this.refs.editor.focus();
|
||||
this._editor.focus();
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
@ -1326,7 +1354,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (displayedCompletion == null) {
|
||||
if (this.state.originalEditorState) {
|
||||
let editorState = this.state.originalEditorState;
|
||||
const editorState = this.state.originalEditorState;
|
||||
this.setState({editorState});
|
||||
}
|
||||
return false;
|
||||
|
@ -1337,7 +1365,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
completion = '',
|
||||
completionId = '',
|
||||
href = null,
|
||||
suffix = ''
|
||||
suffix = '',
|
||||
} = displayedCompletion;
|
||||
|
||||
let inline;
|
||||
|
@ -1345,15 +1373,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
inline = Inline.create({
|
||||
type: 'pill',
|
||||
data: { completion, completionId, href },
|
||||
// we can't put text in here otherwise the editor tries to select it
|
||||
isVoid: true,
|
||||
});
|
||||
} else if (completion === '@room') {
|
||||
inline = Inline.create({
|
||||
type: 'pill',
|
||||
data: { completion, completionId },
|
||||
// we can't put text in here otherwise the editor tries to select it
|
||||
isVoid: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1361,8 +1385,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (range) {
|
||||
const change = editorState.change()
|
||||
.collapseToAnchor()
|
||||
.moveOffsetsTo(range.start, range.end)
|
||||
.moveToAnchor()
|
||||
.moveAnchorTo(range.start)
|
||||
.moveFocusTo(range.end)
|
||||
.focus();
|
||||
editorState = change.value;
|
||||
}
|
||||
|
@ -1373,8 +1398,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
.insertInlineAtRange(editorState.selection, inline)
|
||||
.insertText(suffix)
|
||||
.focus();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
change = editorState.change()
|
||||
.insertTextAtRange(editorState.selection, completion)
|
||||
.insertText(suffix)
|
||||
|
@ -1433,17 +1457,17 @@ export default class MessageComposerInput extends React.Component {
|
|||
room={this.props.room}
|
||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||
isSelected={isSelected}
|
||||
{...attributes}
|
||||
/>;
|
||||
}
|
||||
else if (Pill.isPillUrl(url)) {
|
||||
} else if (Pill.isPillUrl(url)) {
|
||||
return <Pill
|
||||
url={url}
|
||||
room={this.props.room}
|
||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||
isSelected={isSelected}
|
||||
{...attributes}
|
||||
/>;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const { text } = node;
|
||||
return <a href={url} {...props.attributes}>
|
||||
{ text }
|
||||
|
@ -1456,9 +1480,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
|
||||
const shortname = toShort(emojiUnicode);
|
||||
const className = classNames('mx_emojione', {
|
||||
mx_emojione_selected: isSelected
|
||||
mx_emojione_selected: isSelected,
|
||||
});
|
||||
return <img className={ className } src={ uri } title={ shortname } alt={ emojiUnicode }/>;
|
||||
const style = {};
|
||||
if (props.selected) style.border = '1px solid blue';
|
||||
return <img className={ className } src={ uri } title={ shortname } alt={ emojiUnicode } style={style} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1486,7 +1512,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
// of focusing it doesn't then cancel the format button being pressed
|
||||
// FIXME: can we just tell handleKeyCommand's change to invoke .focus()?
|
||||
if (document.activeElement && document.activeElement.className !== 'mx_MessageComposer_editor') {
|
||||
this.refs.editor.focus();
|
||||
this._editor.focus();
|
||||
setTimeout(()=>{
|
||||
this.handleKeyCommand(name);
|
||||
}, 500); // can't find any callback to hook this to. onFocus and onChange and willComponentUpdate fire too early.
|
||||
|
@ -1503,10 +1529,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
// This avoids us having to serialize the whole thing to plaintext and convert
|
||||
// selection offsets in & out of the plaintext domain.
|
||||
|
||||
if (editorState.selection.anchorKey) {
|
||||
return editorState.document.getDescendant(editorState.selection.anchorKey).text;
|
||||
}
|
||||
else {
|
||||
if (editorState.selection.anchor.key) {
|
||||
return editorState.document.getDescendant(editorState.selection.anchor.key).text;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
@ -1518,17 +1543,17 @@ export default class MessageComposerInput extends React.Component {
|
|||
const firstGrandChild = firstChild && firstChild.nodes.get(0);
|
||||
beginning = (firstChild && firstGrandChild &&
|
||||
firstChild.object === 'block' && firstGrandChild.object === 'text' &&
|
||||
editorState.selection.anchorKey === firstGrandChild.key);
|
||||
editorState.selection.anchor.key === firstGrandChild.key);
|
||||
|
||||
// return a character range suitable for handing to an autocomplete provider.
|
||||
// the range is relative to the anchor of the current editor selection.
|
||||
// if the selection spans multiple blocks, then we collapse it for the calculation.
|
||||
const range = {
|
||||
beginning, // whether the selection is in the first block of the editor or not
|
||||
start: editorState.selection.anchorOffset,
|
||||
end: (editorState.selection.anchorKey == editorState.selection.focusKey) ?
|
||||
editorState.selection.focusOffset : editorState.selection.anchorOffset,
|
||||
}
|
||||
start: editorState.selection.anchor.offset,
|
||||
end: (editorState.selection.anchor.key == editorState.selection.focus.key) ?
|
||||
editorState.selection.focus.offset : editorState.selection.anchor.offset,
|
||||
};
|
||||
if (range.start > range.end) {
|
||||
const tmp = range.start;
|
||||
range.start = range.end;
|
||||
|
@ -1543,7 +1568,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
};
|
||||
|
||||
focusComposer = () => {
|
||||
this.refs.editor.focus();
|
||||
this._editor.focus();
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -1553,7 +1578,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
mx_MessageComposer_input_error: this.state.someCompletions === false,
|
||||
});
|
||||
|
||||
const isEmpty = this.state.editorState.document.isEmpty;
|
||||
const isEmpty = Plain.serialize(this.state.editorState) === '';
|
||||
|
||||
let {placeholder} = this.props;
|
||||
// XXX: workaround for placeholder being shown when there is a formatting block e.g blockquote but no text
|
||||
|
@ -1579,7 +1604,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
onMouseDown={this.onMarkdownToggleClicked}
|
||||
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
|
||||
src={`img/button-md-${!this.state.isRichTextEnabled}.png`} />
|
||||
<Editor ref="editor"
|
||||
<Editor ref={this._collectEditor}
|
||||
dir="auto"
|
||||
className="mx_MessageComposer_editor"
|
||||
placeholder={placeholder}
|
||||
|
@ -1591,6 +1616,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
renderMark={this.renderMark}
|
||||
// disable spell check for the placeholder because browsers don't like "unencrypted"
|
||||
spellCheck={!isEmpty}
|
||||
schema={SLATE_SCHEMA}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -41,7 +41,10 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// the RoomMember to show the RR for
|
||||
member: PropTypes.object.isRequired,
|
||||
member: PropTypes.object,
|
||||
// userId to fallback the avatar to
|
||||
// if the member hasn't been loaded yet
|
||||
fallbackUserId: PropTypes.string.isRequired,
|
||||
|
||||
// number of pixels to offset the avatar from the right of its parent;
|
||||
// typically a negative value.
|
||||
|
@ -130,8 +133,7 @@ module.exports = React.createClass({
|
|||
// the docs for `offsetParent` say it may be null if `display` is
|
||||
// `none`, but I can't see why that would happen.
|
||||
console.warn(
|
||||
`ReadReceiptMarker for ${this.props.member.userId} in ` +
|
||||
`${this.props.member.roomId} has no offsetParent`,
|
||||
`ReadReceiptMarker for ${this.props.fallbackUserId} in has no offsetParent`,
|
||||
);
|
||||
startTopOffset = 0;
|
||||
} else {
|
||||
|
@ -186,17 +188,17 @@ module.exports = React.createClass({
|
|||
let title;
|
||||
if (this.props.timestamp) {
|
||||
const dateString = formatDate(new Date(this.props.timestamp), this.props.showTwelveHour);
|
||||
if (this.props.member.userId === this.props.member.rawDisplayName) {
|
||||
if (!this.props.member || this.props.fallbackUserId === this.props.member.rawDisplayName) {
|
||||
title = _t(
|
||||
"Seen by %(userName)s at %(dateTime)s",
|
||||
{userName: this.props.member.userId,
|
||||
{userName: this.props.fallbackUserId,
|
||||
dateTime: dateString},
|
||||
);
|
||||
} else {
|
||||
title = _t(
|
||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
||||
{displayName: this.props.member.rawDisplayName,
|
||||
userName: this.props.member.userId,
|
||||
userName: this.props.fallbackUserId,
|
||||
dateTime: dateString},
|
||||
);
|
||||
}
|
||||
|
@ -208,6 +210,7 @@ module.exports = React.createClass({
|
|||
enterTransitionOpts={this.state.enterTransitionOpts} >
|
||||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
fallbackUserId={this.props.fallbackUserId}
|
||||
aria-hidden="true"
|
||||
width={14} height={14} resizeMethod="crop"
|
||||
style={style}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomDropTarget',
|
||||
|
@ -31,5 +31,5 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -44,9 +44,13 @@ module.exports = React.createClass({
|
|||
error: PropTypes.object,
|
||||
|
||||
canPreview: PropTypes.bool,
|
||||
spinner: PropTypes.bool,
|
||||
room: PropTypes.object,
|
||||
|
||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
||||
// purpose of the spinner.
|
||||
spinner: PropTypes.bool,
|
||||
spinnerState: PropTypes.oneOf(["joining"]),
|
||||
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
|
@ -93,7 +97,12 @@ module.exports = React.createClass({
|
|||
|
||||
if (this.props.spinner || this.state.busy) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
let spinnerIntro = "";
|
||||
if (this.props.spinnerState === "joining") {
|
||||
spinnerIntro = _t("Joining room...");
|
||||
}
|
||||
return (<div className="mx_RoomPreviewBar">
|
||||
<p className="mx_RoomPreviewBar_spinnerIntro">{ spinnerIntro }</p>
|
||||
<Spinner />
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -590,6 +590,11 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_openDevtools: function() {
|
||||
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||
Modal.createDialog(DevtoolsDialog, {roomId: this.props.room.roomId});
|
||||
},
|
||||
|
||||
_renderEncryptionSection: function() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
|
||||
|
@ -942,6 +947,11 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const devtoolsButton = SettingsStore.getValue("showDeveloperTools") ?
|
||||
(<AccessibleButton className="mx_RoomSettings_devtoolsButton" onClick={this._openDevtools}>
|
||||
{ _t("Open Devtools") }
|
||||
</AccessibleButton>) : null;
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings">
|
||||
|
||||
|
@ -1055,6 +1065,7 @@ module.exports = React.createClass({
|
|||
{ _t('Internal room ID: ') } <code>{ this.props.room.roomId }</code><br />
|
||||
{ _t('Room version number: ') } <code>{ this.props.room.getVersion() }</code><br />
|
||||
{ roomUpgradeButton }
|
||||
{ devtoolsButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,11 +16,11 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
var classNames = require('classnames');
|
||||
var AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
const React = require('react');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require('../../../index');
|
||||
const classNames = require('classnames');
|
||||
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -28,7 +28,7 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return ({
|
||||
scope: 'Room'
|
||||
scope: 'Room',
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -54,18 +54,18 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var searchButtonClasses = classNames({ mx_SearchBar_searchButton : true, mx_SearchBar_searching: this.props.searchInProgress });
|
||||
var thisRoomClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'Room' });
|
||||
var allRoomsClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'All' });
|
||||
const searchButtonClasses = classNames({ mx_SearchBar_searchButton: true, mx_SearchBar_searching: this.props.searchInProgress });
|
||||
const thisRoomClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'Room' });
|
||||
const allRoomsClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'All' });
|
||||
|
||||
return (
|
||||
<div className="mx_SearchBar">
|
||||
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange}/>
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")}/></AccessibleButton>
|
||||
<div className="mx_SearchBar">
|
||||
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")} /></AccessibleButton>
|
||||
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick}>{_t("This Room")}</AccessibleButton>
|
||||
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick}>{_t("All Rooms")}</AccessibleButton>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -151,8 +151,8 @@ export default class Stickerpicker extends React.Component {
|
|||
<AccessibleButton onClick={this._launchManageIntegrations}
|
||||
className='mx_Stickers_contentPlaceholder'>
|
||||
<p>{ _t("You don't currently have any stickerpacks enabled") }</p>
|
||||
<p className='mx_Stickers_addLink'>Add some now</p>
|
||||
<img src='img/stickerpack-placeholder.png' alt={_t('Add a stickerpack')} />
|
||||
<p className='mx_Stickers_addLink'>{ _t("Add some now") }</p>
|
||||
<img src='img/stickerpack-placeholder.png' alt="" />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -344,7 +344,7 @@ export default class Stickerpicker extends React.Component {
|
|||
if (this.state.showStickers) {
|
||||
// Show hide-stickers button
|
||||
stickersButton =
|
||||
<div
|
||||
<AccessibleButton
|
||||
id='stickersButton'
|
||||
key="controls_hide_stickers"
|
||||
className="mx_MessageComposer_stickers mx_Stickers_hideStickers"
|
||||
|
@ -352,18 +352,18 @@ export default class Stickerpicker extends React.Component {
|
|||
ref='target'
|
||||
title={_t("Hide Stickers")}>
|
||||
<TintableSvg src="img/icons-hide-stickers.svg" width="35" height="35" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
// Show show-stickers button
|
||||
stickersButton =
|
||||
<div
|
||||
<AccessibleButton
|
||||
id='stickersButton'
|
||||
key="constrols_show_stickers"
|
||||
key="controls_show_stickers"
|
||||
className="mx_MessageComposer_stickers"
|
||||
onClick={this._onShowStickersClick}
|
||||
title={_t("Show Stickers")}>
|
||||
<TintableSvg src="img/icons-show-stickers.svg" width="35" height="35" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
return <div>
|
||||
{stickersButton}
|
||||
|
|
|
@ -164,6 +164,7 @@ export default class DevicesPanel extends React.Component {
|
|||
|
||||
render() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (this.state.deviceLoadError !== undefined) {
|
||||
const classes = classNames(this.props.className, "error");
|
||||
|
@ -185,9 +186,9 @@ export default class DevicesPanel extends React.Component {
|
|||
|
||||
const deleteButton = this.state.deleting ?
|
||||
<Spinner w={22} h={22} /> :
|
||||
<div className="mx_textButton" onClick={this._onDeleteClick}>
|
||||
<AccessibleButton className="mx_textButton" onClick={this._onDeleteClick}>
|
||||
{ _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) }
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
|
||||
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||
return (
|
||||
|
|
|
@ -16,10 +16,10 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var dis = require('../../../dispatcher');
|
||||
const React = require('react');
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const dis = require('../../../dispatcher');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'IntegrationsManager',
|
||||
|
@ -59,5 +59,5 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<iframe src={ this.props.src }></iframe>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
NotificationUtils,
|
||||
VectorPushRulesDefinitions,
|
||||
PushRuleVectorState,
|
||||
ContentRules
|
||||
ContentRules,
|
||||
} from '../../../notifications';
|
||||
|
||||
// TODO: this "view" component still has far too much application logic in it,
|
||||
|
@ -47,7 +47,7 @@ const LEGACY_RULES = {
|
|||
"im.vector.rule.room_message": ".m.rule.message",
|
||||
"im.vector.rule.invite_for_me": ".m.rule.invite_for_me",
|
||||
"im.vector.rule.call": ".m.rule.call",
|
||||
"im.vector.rule.notices": ".m.rule.suppress_notices"
|
||||
"im.vector.rule.notices": ".m.rule.suppress_notices",
|
||||
};
|
||||
|
||||
function portLegacyActions(actions) {
|
||||
|
@ -67,7 +67,7 @@ module.exports = React.createClass({
|
|||
phases: {
|
||||
LOADING: "LOADING", // The component is loading or sending data to the hs
|
||||
DISPLAY: "DISPLAY", // The component is ready and display data
|
||||
ERROR: "ERROR" // There was an error
|
||||
ERROR: "ERROR", // There was an error
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
|
@ -79,7 +79,7 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
threepids: []
|
||||
threepids: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -90,10 +90,10 @@ module.exports = React.createClass({
|
|||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
||||
vectorState: PushRuleVectorState.ON,
|
||||
rules: []
|
||||
rules: [],
|
||||
},
|
||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
||||
externalContentRules: [] // Keyword push rules that have been defined outside Vector UI
|
||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -104,7 +104,7 @@ module.exports = React.createClass({
|
|||
onEnableNotificationsChange: function(event) {
|
||||
const self = this;
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
phase: this.phases.LOADING,
|
||||
});
|
||||
|
||||
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !event.target.checked).done(function() {
|
||||
|
@ -145,7 +145,7 @@ module.exports = React.createClass({
|
|||
onEnableEmailNotificationsChange: function(address, event) {
|
||||
let emailPusherPromise;
|
||||
if (event.target.checked) {
|
||||
const data = {}
|
||||
const data = {};
|
||||
data['brand'] = this.props.brand || 'Riot';
|
||||
emailPusherPromise = UserSettingsStore.addEmailPusher(address, data);
|
||||
} else {
|
||||
|
@ -170,9 +170,8 @@ module.exports = React.createClass({
|
|||
const newPushRuleVectorState = event.target.className.split("-")[1];
|
||||
|
||||
if ("_keywords" === vectorRuleId) {
|
||||
this._setKeywordsPushRuleVectorState(newPushRuleVectorState)
|
||||
}
|
||||
else {
|
||||
this._setKeywordsPushRuleVectorState(newPushRuleVectorState);
|
||||
} else {
|
||||
const rule = this.getRule(vectorRuleId);
|
||||
if (rule) {
|
||||
this._setPushRuleVectorState(rule, newPushRuleVectorState);
|
||||
|
@ -185,7 +184,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Compute the keywords list to display
|
||||
let keywords = [];
|
||||
for (let i in this.state.vectorContentRules.rules) {
|
||||
for (const i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
keywords.push(rule.pattern);
|
||||
}
|
||||
|
@ -195,8 +194,7 @@ module.exports = React.createClass({
|
|||
keywords.sort();
|
||||
|
||||
keywords = keywords.join(", ");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
keywords = "";
|
||||
}
|
||||
|
||||
|
@ -207,29 +205,28 @@ module.exports = React.createClass({
|
|||
button: _t('OK'),
|
||||
value: keywords,
|
||||
onFinished: function onFinished(should_leave, newValue) {
|
||||
|
||||
if (should_leave && newValue !== keywords) {
|
||||
let newKeywords = newValue.split(',');
|
||||
for (let i in newKeywords) {
|
||||
for (const i in newKeywords) {
|
||||
newKeywords[i] = newKeywords[i].trim();
|
||||
}
|
||||
|
||||
// Remove duplicates and empty
|
||||
newKeywords = newKeywords.reduce(function(array, keyword){
|
||||
newKeywords = newKeywords.reduce(function(array, keyword) {
|
||||
if (keyword !== "" && array.indexOf(keyword) < 0) {
|
||||
array.push(keyword);
|
||||
}
|
||||
return array;
|
||||
},[]);
|
||||
}, []);
|
||||
|
||||
self._setKeywords(newKeywords);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
getRule: function(vectorRuleId) {
|
||||
for (let i in this.state.vectorPushRules) {
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
if (rule.vectorRuleId === vectorRuleId) {
|
||||
return rule;
|
||||
|
@ -239,9 +236,8 @@ module.exports = React.createClass({
|
|||
|
||||
_setPushRuleVectorState: function(rule, newPushRuleVectorState) {
|
||||
if (rule && rule.vectorState !== newPushRuleVectorState) {
|
||||
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
phase: this.phases.LOADING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
@ -255,8 +251,7 @@ module.exports = React.createClass({
|
|||
if (!actions) {
|
||||
// The new state corresponds to disabling the rule.
|
||||
deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// The new state corresponds to enabling the rule and setting specific actions
|
||||
deferreds.push(this._updatePushRuleActions(rule.rule, actions, true));
|
||||
}
|
||||
|
@ -270,7 +265,7 @@ module.exports = React.createClass({
|
|||
Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, {
|
||||
title: _t('Failed to change settings'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -287,12 +282,12 @@ module.exports = React.createClass({
|
|||
const cli = MatrixClientPeg.get();
|
||||
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
phase: this.phases.LOADING,
|
||||
});
|
||||
|
||||
// Update all rules in self.state.vectorContentRules
|
||||
const deferreds = [];
|
||||
for (let i in this.state.vectorContentRules.rules) {
|
||||
for (const i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
|
||||
let enabled, actions;
|
||||
|
@ -326,8 +321,7 @@ module.exports = React.createClass({
|
|||
// Note that the workaround in _updatePushRuleActions will automatically
|
||||
// enable the rule
|
||||
deferreds.push(this._updatePushRuleActions(rule, actions, enabled));
|
||||
}
|
||||
else if (enabled != undefined) {
|
||||
} else if (enabled != undefined) {
|
||||
deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled));
|
||||
}
|
||||
}
|
||||
|
@ -340,14 +334,14 @@ module.exports = React.createClass({
|
|||
Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, {
|
||||
title: _t('Can\'t update user notification settings'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_setKeywords: function(newKeywords) {
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
phase: this.phases.LOADING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
@ -356,7 +350,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Remove per-word push rules of keywords that are no more in the list
|
||||
const vectorContentRulesPatterns = [];
|
||||
for (let i in self.state.vectorContentRules.rules) {
|
||||
for (const i in self.state.vectorContentRules.rules) {
|
||||
const rule = self.state.vectorContentRules.rules[i];
|
||||
|
||||
vectorContentRulesPatterns.push(rule.pattern);
|
||||
|
@ -368,7 +362,7 @@ module.exports = React.createClass({
|
|||
|
||||
// If the keyword is part of `externalContentRules`, remove the rule
|
||||
// before recreating it in the right Vector path
|
||||
for (let i in self.state.externalContentRules) {
|
||||
for (const i in self.state.externalContentRules) {
|
||||
const rule = self.state.externalContentRules[i];
|
||||
|
||||
if (newKeywords.indexOf(rule.pattern) >= 0) {
|
||||
|
@ -382,9 +376,9 @@ module.exports = React.createClass({
|
|||
Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, {
|
||||
title: _t('Failed to update keywords'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Then, add the new ones
|
||||
Promise.all(removeDeferreds).done(function(resps) {
|
||||
|
@ -398,14 +392,13 @@ module.exports = React.createClass({
|
|||
// Thus, this new rule will join the 'vectorContentRules' set.
|
||||
if (self.state.vectorContentRules.rules.length) {
|
||||
pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(self.state.vectorContentRules.rules[0]);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// ON is default
|
||||
pushRuleVectorStateKind = PushRuleVectorState.ON;
|
||||
pushRuleVectorStateKind = PushRuleVectorState.ON;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i in newKeywords) {
|
||||
for (const i in newKeywords) {
|
||||
const keyword = newKeywords[i];
|
||||
|
||||
if (vectorContentRulesPatterns.indexOf(keyword) < 0) {
|
||||
|
@ -413,13 +406,12 @@ module.exports = React.createClass({
|
|||
deferreds.push(cli.addPushRule
|
||||
('global', 'content', keyword, {
|
||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
||||
pattern: keyword
|
||||
pattern: keyword,
|
||||
}));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
|
||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
||||
pattern: keyword
|
||||
pattern: keyword,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -435,7 +427,7 @@ module.exports = React.createClass({
|
|||
_addDisabledPushRule: function(scope, kind, ruleId, body) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.addPushRule(scope, kind, ruleId, body).then(() =>
|
||||
cli.setPushRuleEnabled(scope, kind, ruleId, false)
|
||||
cli.setPushRuleEnabled(scope, kind, ruleId, false),
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -446,7 +438,7 @@ module.exports = React.createClass({
|
|||
const needsUpdate = [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
for (let kind in rulesets.global) {
|
||||
for (const kind in rulesets.global) {
|
||||
const ruleset = rulesets.global[kind];
|
||||
for (let i = 0; i < ruleset.length; ++i) {
|
||||
const rule = ruleset[i];
|
||||
|
@ -454,9 +446,9 @@ module.exports = React.createClass({
|
|||
console.log("Porting legacy rule", rule);
|
||||
needsUpdate.push( function(kind, rule) {
|
||||
return cli.setPushRuleActions(
|
||||
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions)
|
||||
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions),
|
||||
).then(() =>
|
||||
cli.deletePushRule('global', kind, rule.rule_id)
|
||||
cli.deletePushRule('global', kind, rule.rule_id),
|
||||
).catch( (e) => {
|
||||
console.warn(`Error when porting legacy rule: ${e}`);
|
||||
});
|
||||
|
@ -469,7 +461,7 @@ module.exports = React.createClass({
|
|||
// If some of the rules need to be ported then wait for the porting
|
||||
// to happen and then fetch the rules again.
|
||||
return Promise.all(needsUpdate).then(() =>
|
||||
cli.getPushRules()
|
||||
cli.getPushRules(),
|
||||
);
|
||||
} else {
|
||||
// Otherwise return the rules that we already have.
|
||||
|
@ -480,7 +472,6 @@ module.exports = React.createClass({
|
|||
_refreshFromServer: function() {
|
||||
const self = this;
|
||||
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) {
|
||||
|
||||
/// XXX seriously? wtf is this?
|
||||
MatrixClientPeg.get().pushRules = rulesets;
|
||||
|
||||
|
@ -497,7 +488,7 @@ module.exports = React.createClass({
|
|||
'.m.rule.invite_for_me': 'vector',
|
||||
//'.m.rule.member_event': 'vector',
|
||||
'.m.rule.call': 'vector',
|
||||
'.m.rule.suppress_notices': 'vector'
|
||||
'.m.rule.suppress_notices': 'vector',
|
||||
|
||||
// Others go to others
|
||||
};
|
||||
|
@ -505,7 +496,7 @@ module.exports = React.createClass({
|
|||
// HS default rules
|
||||
const defaultRules = {master: [], vector: {}, others: []};
|
||||
|
||||
for (let kind in rulesets.global) {
|
||||
for (const kind in rulesets.global) {
|
||||
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
|
||||
const r = rulesets.global[kind][i];
|
||||
const cat = rule_categories[r.rule_id];
|
||||
|
@ -514,11 +505,9 @@ module.exports = React.createClass({
|
|||
if (r.rule_id[0] === '.') {
|
||||
if (cat === 'vector') {
|
||||
defaultRules.vector[r.rule_id] = r;
|
||||
}
|
||||
else if (cat === 'master') {
|
||||
} else if (cat === 'master') {
|
||||
defaultRules.master.push(r);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
defaultRules['others'].push(r);
|
||||
}
|
||||
}
|
||||
|
@ -551,9 +540,9 @@ module.exports = React.createClass({
|
|||
'.m.rule.invite_for_me',
|
||||
//'im.vector.rule.member_event',
|
||||
'.m.rule.call',
|
||||
'.m.rule.suppress_notices'
|
||||
'.m.rule.suppress_notices',
|
||||
];
|
||||
for (let i in vectorRuleIds) {
|
||||
for (const i in vectorRuleIds) {
|
||||
const vectorRuleId = vectorRuleIds[i];
|
||||
|
||||
if (vectorRuleId === '_keywords') {
|
||||
|
@ -562,20 +551,19 @@ module.exports = React.createClass({
|
|||
// it corresponds to all content push rules (stored in self.state.vectorContentRule)
|
||||
self.state.vectorPushRules.push({
|
||||
"vectorRuleId": "_keywords",
|
||||
"description" : (
|
||||
"description": (
|
||||
<span>
|
||||
{ _t('Messages containing <span>keywords</span>',
|
||||
{},
|
||||
{ 'span': (sub) =>
|
||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>
|
||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
"vectorState": self.state.vectorContentRules.vectorState
|
||||
"vectorState": self.state.vectorContentRules.vectorState,
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId];
|
||||
const rule = defaultRules.vector[vectorRuleId];
|
||||
|
||||
|
@ -585,7 +573,7 @@ module.exports = React.createClass({
|
|||
|
||||
self.state.vectorPushRules.push({
|
||||
"vectorRuleId": vectorRuleId,
|
||||
"description" : _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
|
||||
"description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
|
||||
"rule": rule,
|
||||
"vectorState": vectorState,
|
||||
});
|
||||
|
@ -604,7 +592,7 @@ module.exports = React.createClass({
|
|||
'.m.rule.fallback': _t('Notify me for anything else'),
|
||||
};
|
||||
|
||||
for (let i in defaultRules.others) {
|
||||
for (const i in defaultRules.others) {
|
||||
const rule = defaultRules.others[i];
|
||||
const ruleDescription = otherRulesDescriptions[rule.rule_id];
|
||||
|
||||
|
@ -622,12 +610,12 @@ module.exports = React.createClass({
|
|||
|
||||
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
|
||||
self.setState({
|
||||
phase: self.phases.DISPLAY
|
||||
phase: self.phases.DISPLAY,
|
||||
});
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
self.setState({
|
||||
phase: self.phases.ERROR
|
||||
phase: self.phases.ERROR,
|
||||
});
|
||||
}).finally(() => {
|
||||
// actually explicitly update our state having been deep-manipulating it
|
||||
|
@ -645,12 +633,12 @@ module.exports = React.createClass({
|
|||
const cli = MatrixClientPeg.get();
|
||||
|
||||
return cli.setPushRuleActions(
|
||||
'global', rule.kind, rule.rule_id, actions
|
||||
'global', rule.kind, rule.rule_id, actions,
|
||||
).then( function() {
|
||||
// Then, if requested, enabled or disabled the rule
|
||||
if (undefined != enabled) {
|
||||
return cli.setPushRuleEnabled(
|
||||
'global', rule.kind, rule.rule_id, enabled
|
||||
'global', rule.kind, rule.rule_id, enabled,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -689,7 +677,7 @@ module.exports = React.createClass({
|
|||
|
||||
renderNotifRulesTableRows: function() {
|
||||
const rows = [];
|
||||
for (let i in this.state.vectorPushRules) {
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
|
||||
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
|
||||
|
@ -769,20 +757,20 @@ module.exports = React.createClass({
|
|||
// This only supports the first email address in your profile for now
|
||||
emailNotificationsRow = this.emailNotificationsRow(
|
||||
emailThreepids[0].address,
|
||||
`${_t('Enable email notifications')} (${emailThreepids[0].address})`
|
||||
`${_t('Enable email notifications')} (${emailThreepids[0].address})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Build external push rules
|
||||
const externalRules = [];
|
||||
for (let i in this.state.externalPushRules) {
|
||||
for (const i in this.state.externalPushRules) {
|
||||
const rule = this.state.externalPushRules[i];
|
||||
externalRules.push(<li>{ _t(rule.description) }</li>);
|
||||
}
|
||||
|
||||
// Show keywords not displayed by the vector UI as a single external push rule
|
||||
let externalKeywords = [];
|
||||
for (let i in this.state.externalContentRules) {
|
||||
for (const i in this.state.externalContentRules) {
|
||||
const rule = this.state.externalContentRules[i];
|
||||
externalKeywords.push(rule.pattern);
|
||||
}
|
||||
|
@ -793,7 +781,7 @@ module.exports = React.createClass({
|
|||
|
||||
let devicesSection;
|
||||
if (this.state.pushers === undefined) {
|
||||
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>
|
||||
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>;
|
||||
} else if (this.state.pushers.length == 0) {
|
||||
devicesSection = null;
|
||||
} else {
|
||||
|
@ -824,7 +812,7 @@ module.exports = React.createClass({
|
|||
advancedSettings = (
|
||||
<div>
|
||||
<h3>{ _t('Advanced notification settings') }</h3>
|
||||
{ _t('There are advanced notifications which are not shown here') }.<br/>
|
||||
{ _t('There are advanced notifications which are not shown here') }.<br />
|
||||
{ _t('You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply') }.
|
||||
<ul>
|
||||
{ externalRules }
|
||||
|
@ -915,5 +903,5 @@ module.exports = React.createClass({
|
|||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -125,14 +125,15 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const VideoView = sdk.getComponent('voip.VideoView');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let voice;
|
||||
if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) {
|
||||
const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
|
||||
voice = (
|
||||
<div className="mx_CallView_voice" onClick={this.props.onClick}>
|
||||
<AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
|
||||
{ _t("Active call (%(roomName)s)", {roomName: callRoom.name}) }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue