merge develop
This commit is contained in:
commit
efdc5430d7
176 changed files with 7537 additions and 3401 deletions
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2018 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import {Group} from 'matrix-js-sdk';
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
|
||||
export default class GroupInviteTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
group: PropTypes.instanceOf(Group).isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._onClickReject = this._onClickReject.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onClickReject() {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
|
||||
title: _t('Reject invitation'),
|
||||
description: _t('Are you sure you want to reject the invitation?'),
|
||||
onFinished: async (shouldLeave) => {
|
||||
if (!shouldLeave) return;
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
try {
|
||||
await GroupStore.leaveGroup(this.props.group.groupId);
|
||||
} catch (e) {
|
||||
console.error("Error rejecting community invite: ", e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Unable to reject invite"),
|
||||
});
|
||||
} finally {
|
||||
modal.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<div className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||
{ _t('Reject') }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -15,10 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
|
@ -184,6 +183,15 @@ module.exports = React.createClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
onPermalinkClick: function(e: Event) {
|
||||
e.preventDefault();
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReplyClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
|
@ -211,7 +219,10 @@ module.exports = React.createClass({
|
|||
let replyButton;
|
||||
let collapseReplyThread;
|
||||
|
||||
if (eventStatus === 'not_sent') {
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
|
||||
if (eventStatus === EventStatus.NOT_SENT) {
|
||||
resendButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
{ _t('Resend') }
|
||||
|
@ -219,7 +230,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (!eventStatus && this.state.canRedact) {
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
|
@ -227,7 +238,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (eventStatus === "queued" || eventStatus === "not_sent") {
|
||||
if (eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT) {
|
||||
cancelButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
{ _t('Cancel Sending') }
|
||||
|
@ -235,7 +246,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (!eventStatus && this.props.mxEvent.getType() === 'm.room.message') {
|
||||
if (isSent && this.props.mxEvent.getType() === 'm.room.message') {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||
forwardButton = (
|
||||
|
@ -244,13 +255,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_rich_quoting")) {
|
||||
replyButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||
{ _t('Reply') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
replyButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||
{ _t('Reply') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
|
@ -290,7 +299,7 @@ module.exports = React.createClass({
|
|||
const permalinkButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId())}
|
||||
target="_blank" rel="noopener" onClick={this.closeMenu}>{ _t('Permalink') }</a>
|
||||
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>{ _t('Share Message') }</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 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.
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Promise from 'bluebird';
|
||||
|
@ -27,6 +27,13 @@ import GroupStore from '../../../stores/GroupStore';
|
|||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
const addressTypeName = {
|
||||
'mx-user-id': _td("Matrix ID"),
|
||||
'mx-room-id': _td("Matrix Room ID"),
|
||||
'email': _td("email address"),
|
||||
};
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "AddressPickerDialog",
|
||||
|
||||
|
@ -66,7 +73,7 @@ module.exports = React.createClass({
|
|||
|
||||
// List of UserAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
userList: [],
|
||||
selectedList: [],
|
||||
|
||||
// Whether a search is ongoing
|
||||
busy: false,
|
||||
|
@ -76,10 +83,9 @@ module.exports = React.createClass({
|
|||
serverSupportsUserDirectory: true,
|
||||
// The query being searched for
|
||||
query: "",
|
||||
// List of UserAddressType objects representing
|
||||
// the set of auto-completion results for the current search
|
||||
// query.
|
||||
queryList: [],
|
||||
// List of UserAddressType objects representing the set of
|
||||
// auto-completion results for the current search query.
|
||||
suggestedList: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -91,14 +97,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onButtonClick: function() {
|
||||
let userList = this.state.userList.slice();
|
||||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local userList
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.refs.textinput.value !== '') {
|
||||
userList = this._addInputToList();
|
||||
if (userList === null) return;
|
||||
selectedList = this._addInputToList();
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, userList);
|
||||
this.props.onFinished(true, selectedList);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
|
@ -118,18 +124,18 @@ module.exports = React.createClass({
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
} else if (this.state.suggestedList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.onDismissed(this.state.userList.length - 1)();
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.refs.textinput.value == '') {
|
||||
if (this.refs.textinput.value === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
|
@ -148,7 +154,7 @@ module.exports = React.createClass({
|
|||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
if (query.length > 0 && query !== '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.props.pickerType === 'user') {
|
||||
if (this.props.groupId) {
|
||||
|
@ -170,7 +176,7 @@ module.exports = React.createClass({
|
|||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
queryList: [],
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
searchError: null,
|
||||
});
|
||||
|
@ -179,11 +185,11 @@ module.exports = React.createClass({
|
|||
|
||||
onDismissed: function(index) {
|
||||
return () => {
|
||||
const userList = this.state.userList.slice();
|
||||
userList.splice(index, 1);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.splice(index, 1);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
|
@ -197,11 +203,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onSelected: function(index) {
|
||||
const userList = this.state.userList.slice();
|
||||
userList.push(this.state.queryList[index]);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(this.state.suggestedList[index]);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
|
@ -379,10 +385,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
const suggestedList = [];
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
queryList.push({
|
||||
suggestedList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
|
@ -399,7 +405,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
queryList.push({
|
||||
suggestedList.push({
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
|
@ -413,18 +419,18 @@ module.exports = React.createClass({
|
|||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (this.props.validAddressTypes.includes(addrType)) {
|
||||
queryList.unshift({
|
||||
suggestedList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
if (addrType === 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
queryList,
|
||||
suggestedList,
|
||||
error: false,
|
||||
}, () => {
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
||||
|
@ -442,14 +448,14 @@ module.exports = React.createClass({
|
|||
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType == 'mx-user-id') {
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType == 'mx-room-id') {
|
||||
} else if (addrType === 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
|
@ -458,15 +464,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const userList = this.state.userList.slice();
|
||||
userList.push(addrObj);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(addrObj);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return userList;
|
||||
return selectedList;
|
||||
},
|
||||
|
||||
_lookupThreepid: function(medium, address) {
|
||||
|
@ -492,7 +498,7 @@ module.exports = React.createClass({
|
|||
if (res === null) return null;
|
||||
if (cancelled) return null;
|
||||
this.setState({
|
||||
queryList: [{
|
||||
suggestedList: [{
|
||||
// a UserAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
|
@ -510,15 +516,27 @@ module.exports = React.createClass({
|
|||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
// map addressType => set of addresses to avoid O(n*m) operation
|
||||
const selectedAddresses = {};
|
||||
this.state.selectedList.forEach(({address, addressType}) => {
|
||||
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
|
||||
selectedAddresses[addressType].add(address);
|
||||
});
|
||||
|
||||
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||
const filteredSuggestedList = this.state.suggestedList.filter(({address, addressType}) => {
|
||||
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||
});
|
||||
|
||||
const query = [];
|
||||
// create the invite list
|
||||
if (this.state.userList.length > 0) {
|
||||
if (this.state.selectedList.length > 0) {
|
||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
for (let i = 0; i < this.state.userList.length; i++) {
|
||||
for (let i = 0; i < this.state.selectedList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.userList[i]}
|
||||
address={this.state.selectedList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
|
@ -528,7 +546,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Add the query at the end
|
||||
query.push(
|
||||
<textarea key={this.state.userList.length}
|
||||
<textarea key={this.state.selectedList.length}
|
||||
rows="1"
|
||||
id="textinput"
|
||||
ref="textinput"
|
||||
|
@ -543,34 +561,22 @@ module.exports = React.createClass({
|
|||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
let tryUsing = '';
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => {
|
||||
return {
|
||||
'mx-user-id': _t("Matrix ID"),
|
||||
'mx-room-id': _t("Matrix Room ID"),
|
||||
'email': _t("email address"),
|
||||
}[t];
|
||||
});
|
||||
tryUsing = _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
});
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||
error = <div className="mx_ChatInviteDialog_error">
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
{ tryUsing }
|
||||
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
}) }
|
||||
</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||
} else if (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={this.state.queryList}
|
||||
addressList={filteredSuggestedList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
|
@ -64,7 +65,10 @@ export default React.createClass({
|
|||
|
||||
// Id of content element
|
||||
// If provided, this is used to add a aria-describedby attribute
|
||||
contentId: React.PropTypes.string,
|
||||
contentId: PropTypes.string,
|
||||
|
||||
// optional additional class for the title element
|
||||
titleClass: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -105,25 +109,28 @@ export default React.createClass({
|
|||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let cancelButton;
|
||||
if (this.props.hasCancel) {
|
||||
cancelButton = <AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton">
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FocusTrap onKeyDown={this._onKeyDown}
|
||||
className={this.props.className}
|
||||
role="dialog"
|
||||
aria-labelledby='mx_BaseDialog_title'
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completelly follow this recommendation we'd need to
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
aria-describedby={this.props.contentId}
|
||||
>
|
||||
{ this.props.hasCancel ? <AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton> : null }
|
||||
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
|
||||
{ cancelButton }
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
|
|
|
@ -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.
|
||||
|
@ -28,6 +29,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onFinished = this.onFinished.bind(this);
|
||||
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
|
@ -53,10 +55,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
const room = client.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(client.credentials.userId);
|
||||
const highlight = (
|
||||
room.getUnreadNotificationCount('highlight') > 0 ||
|
||||
me.membership == "invite"
|
||||
);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === "invite";
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
transparent={true}
|
||||
|
@ -64,7 +63,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership == "invite"}
|
||||
isInvite={me.membership === "invite"}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
|
@ -110,6 +109,10 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
this.props.onExistingRoomSelected(roomId);
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = '';
|
||||
let content = null;
|
||||
|
@ -170,14 +173,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
{ profile }
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus={true} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={this.props.onFinished.bind(false)}
|
||||
onFinished={this.onFinished}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
|
@ -187,7 +190,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
ChatCreateOrReuseDialog.propTyps = {
|
||||
ChatCreateOrReuseDialog.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
// Called when clicking outside of the dialog
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
|
|
@ -52,8 +52,8 @@ export default React.createClass({
|
|||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||
<div className="mx_CreateRoomDialog_input_container">
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
|
|
|
@ -33,10 +33,21 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this._onOk = this._onOk.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
const deactivationPreferences =
|
||||
MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences');
|
||||
|
||||
const shouldErase = (
|
||||
deactivationPreferences &&
|
||||
deactivationPreferences.getContent() &&
|
||||
deactivationPreferences.getContent().shouldErase
|
||||
) || false;
|
||||
|
||||
this.state = {
|
||||
confirmButtonEnabled: false,
|
||||
busy: false,
|
||||
shouldErase,
|
||||
errStr: null,
|
||||
};
|
||||
}
|
||||
|
@ -47,19 +58,55 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onOk() {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
_onEraseFieldChange(ev) {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
async _onOk() {
|
||||
this.setState({busy: true});
|
||||
MatrixClientPeg.get().deactivateAccount({
|
||||
type: 'm.login.password',
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
password: this._passwordField.value,
|
||||
}).done(() => {
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(false);
|
||||
}, (err) => {
|
||||
|
||||
// Before we deactivate the account insert an event into
|
||||
// the user's account data indicating that they wish to be
|
||||
// erased from the homeserver.
|
||||
//
|
||||
// We do this because the API for erasing after deactivation
|
||||
// might not be supported by the connected homeserver. Leaving
|
||||
// an indication in account data is only best-effort, and
|
||||
// in the worse case, the HS maintainer would have to run a
|
||||
// script to erase deactivated accounts that have shouldErase
|
||||
// set to true in im.riot.account_deactivation_preferences.
|
||||
//
|
||||
// Note: The preferences are scoped to Riot, hence the
|
||||
// "im.riot..." event type.
|
||||
//
|
||||
// Note: This may have already been set on previous attempts
|
||||
// where, for example, the user entered the wrong password.
|
||||
// This is fine because the UI always indicates the preference
|
||||
// prior to us calling `deactivateAccount`.
|
||||
try {
|
||||
await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', {
|
||||
shouldErase: this.state.shouldErase,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errStr: _t('Failed to indicate account erasure'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
const auth = {
|
||||
type: 'm.login.password',
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
password: this._passwordField.value,
|
||||
};
|
||||
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
|
||||
} catch (err) {
|
||||
let errStr = _t('Unknown error');
|
||||
// https://matrix.org/jira/browse/SYN-744
|
||||
if (err.httpStatus == 401 || err.httpStatus == 403) {
|
||||
|
@ -70,7 +117,12 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
busy: false,
|
||||
errStr: errStr,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
|
@ -105,21 +157,64 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
titleClass="danger"
|
||||
title={_t("Deactivate Account")}>
|
||||
title={_t("Deactivate Account")}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
||||
<p>{ _t(
|
||||
"This will make your account permanently unusable. " +
|
||||
"You will not be able to log in, and no one will be able to re-register the same " +
|
||||
"user ID. " +
|
||||
"This will cause your account to leave all rooms it is participating in, and it " +
|
||||
"will remove your account details from your identity server. " +
|
||||
"<b>This action is irreversible.</b>",
|
||||
{},
|
||||
{ b: (sub) => <b> { sub } </b> },
|
||||
) }</p>
|
||||
|
||||
<p>{ _t("This action is irreversible.") }</p>
|
||||
<p>{ _t(
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you " +
|
||||
"have sent.</b> " +
|
||||
"If you would like us to forget your messages, please tick the box below.",
|
||||
{},
|
||||
{ b: (sub) => <b> { sub } </b> },
|
||||
) }</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
<p>{ _t(
|
||||
"Message visibility in Matrix is similar to email. " +
|
||||
"Our forgetting your messages means that messages you have sent will not be shared " +
|
||||
"with any new or unregistered users, but registered users who already have access " +
|
||||
"to these messages will still have access to their copy.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_DeactivateAccountDialog_input_section">
|
||||
<p>
|
||||
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
|
||||
<input
|
||||
id="mx_DeactivateAccountDialog_erase_account_input"
|
||||
type="checkbox"
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
/>
|
||||
{ _t(
|
||||
"Please forget all messages I have sent when my account is deactivated " +
|
||||
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
|
||||
"of conversations)",
|
||||
{},
|
||||
{ b: (sub) => <b>{ sub }</b> },
|
||||
) }
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password:") }</p>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={_t("password")}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p>{ _t("Password") }:</p>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this._onPasswordFieldChange}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
{ error }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
|
|
@ -132,17 +132,17 @@ class SendCustomEvent extends GenericEditor {
|
|||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
|
||||
<br />
|
||||
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -219,15 +219,15 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
<br />
|
||||
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -242,6 +242,9 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
}
|
||||
|
||||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
|
@ -249,31 +252,65 @@ class FilteredList extends React.Component {
|
|||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static filterChildren(children, query) {
|
||||
if (!query) return children;
|
||||
const lcQuery = query.toLowerCase();
|
||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onQuery = this.onQuery.bind(this);
|
||||
|
||||
this.state = {
|
||||
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
};
|
||||
}
|
||||
|
||||
onQuery(ev) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||
this.setState({
|
||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
});
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
this.setState({
|
||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||
});
|
||||
};
|
||||
|
||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||
</button>;
|
||||
};
|
||||
|
||||
onQuery = (ev) => {
|
||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
filterChildren() {
|
||||
if (this.props.query) {
|
||||
const lowerQuery = this.props.query.toLowerCase();
|
||||
return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
getChildren = (start: number, end: number) => {
|
||||
return this.state.filteredChildren.slice(start, end);
|
||||
};
|
||||
|
||||
getChildCount = (): number => {
|
||||
return this.state.filteredChildren.length;
|
||||
};
|
||||
|
||||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<input size="64"
|
||||
onChange={this.onQuery}
|
||||
value={this.props.query}
|
||||
placeholder={_t('Filter results')}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
|
||||
{ this.filterChildren() }
|
||||
<TruncatedList getChildren={this.getChildren}
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this.createOverflowElement} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -485,7 +522,7 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
}
|
||||
|
||||
return <div className="mx_ViewSource">
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.state.event.event, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
|
|
|
@ -67,8 +67,10 @@ export default React.createClass({
|
|||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
>
|
||||
|
|
|
@ -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.
|
||||
|
@ -36,7 +37,7 @@ export default React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
emailAddress: null,
|
||||
emailAddress: '',
|
||||
emailBusy: false,
|
||||
};
|
||||
},
|
||||
|
@ -127,6 +128,7 @@ export default React.createClass({
|
|||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
initialValue={this.state.emailAddress}
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
autoFocus="true"
|
||||
placeholder={_t("Email address")}
|
||||
|
|
|
@ -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.
|
||||
|
@ -79,15 +80,11 @@ export default React.createClass({
|
|||
Modal.createDialog(WarmFuzzy, {
|
||||
didSetEmail: res.didSetEmail,
|
||||
onFinished: () => {
|
||||
this._onContinueClicked();
|
||||
this.props.onFinished();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onContinueClicked: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
_onPasswordChangeError: function(err) {
|
||||
let errMsg = err.error || "";
|
||||
if (err.httpStatus === 403) {
|
||||
|
|
224
src/components/views/dialogs/ShareDialog.js
Normal file
224
src/components/views/dialogs/ShareDialog.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import {makeEventPermalink, makeGroupPermalink, makeRoomPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
img: 'img/social/facebook.png',
|
||||
url: (url) => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
||||
}, {
|
||||
name: 'Twitter',
|
||||
img: 'img/social/twitter-2.png',
|
||||
url: (url) => `https://twitter.com/home?status=${url}`,
|
||||
}, /* // icon missing
|
||||
name: 'Google Plus',
|
||||
img: 'img/social/',
|
||||
url: (url) => `https://plus.google.com/share?url=${url}`,
|
||||
},*/ {
|
||||
name: 'LinkedIn',
|
||||
img: 'img/social/linkedin.png',
|
||||
url: (url) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`,
|
||||
}, {
|
||||
name: 'Reddit',
|
||||
img: 'img/social/reddit.png',
|
||||
url: (url) => `http://www.reddit.com/submit?url=${url}`,
|
||||
}, {
|
||||
name: 'email',
|
||||
img: 'img/social/email-1.png',
|
||||
url: (url) => `mailto:?body=${url}`,
|
||||
},
|
||||
];
|
||||
|
||||
export default class ShareDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
target: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Room),
|
||||
PropTypes.instanceOf(User),
|
||||
PropTypes.instanceOf(Group),
|
||||
PropTypes.instanceOf(RoomMember),
|
||||
PropTypes.instanceOf(MatrixEvent),
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onCopyClick = this.onCopyClick.bind(this);
|
||||
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
// MatrixEvent defaults to share linkSpecificEvent
|
||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||
};
|
||||
}
|
||||
|
||||
static _selectText(target) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
static onLinkClick(e) {
|
||||
e.preventDefault();
|
||||
const {target} = e;
|
||||
ShareDialog._selectText(target);
|
||||
}
|
||||
|
||||
onCopyClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
ShareDialog._selectText(this.refs.link);
|
||||
|
||||
let successful;
|
||||
try {
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||
chevronOffset: 10,
|
||||
left: x,
|
||||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
}
|
||||
|
||||
onLinkSpecificEventCheckboxClick() {
|
||||
this.setState({
|
||||
linkSpecificEvent: !this.state.linkSpecificEvent,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let matrixToUrl;
|
||||
|
||||
let checkbox;
|
||||
|
||||
if (this.props.target instanceof Room) {
|
||||
title = _t('Share Room');
|
||||
|
||||
const events = this.props.target.getLiveTimeline().getEvents();
|
||||
if (events.length > 0) {
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to most recent message') }
|
||||
</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.roomId, events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.roomId);
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
} else if (this.props.target instanceof Group) {
|
||||
title = _t('Share Community');
|
||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
title = _t('Share Room Message');
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to selected message') }
|
||||
</label>
|
||||
</div>;
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.getRoomId(), this.props.target.getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.getRoomId());
|
||||
}
|
||||
}
|
||||
|
||||
const encodedUrl = encodeURIComponent(matrixToUrl);
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return <BaseDialog title={title}
|
||||
className='mx_ShareDialog'
|
||||
contentId='mx_Dialog_content'
|
||||
onFinished={this.props.onFinished}
|
||||
>
|
||||
<div className="mx_ShareDialog_content">
|
||||
<div className="mx_ShareDialog_matrixto">
|
||||
<a ref="link"
|
||||
href={matrixToUrl}
|
||||
onClick={ShareDialog.onLinkClick}
|
||||
className="mx_ShareDialog_matrixto_link"
|
||||
>
|
||||
{ matrixToUrl }
|
||||
</a>
|
||||
<a href={matrixToUrl} className="mx_ShareDialog_matrixto_copy" onClick={this.onCopyClick}>
|
||||
{ _t('COPY') }
|
||||
<div> </div>
|
||||
</a>
|
||||
</div>
|
||||
{ checkbox }
|
||||
<hr />
|
||||
|
||||
<div className="mx_ShareDialog_split">
|
||||
<div className="mx_ShareDialog_qrcode_container">
|
||||
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo="img/matrix-m.svg" />
|
||||
</div>
|
||||
<div className="mx_ShareDialog_social_container">
|
||||
{
|
||||
socials.map((social) => <a rel="noopener"
|
||||
target="_blank"
|
||||
key={social.name}
|
||||
name={social.name}
|
||||
href={social.url(encodedUrl)}
|
||||
className="mx_ShareDialog_social_icon"
|
||||
>
|
||||
<img src={social.img} alt={social.name} height={64} width={64} />
|
||||
</a>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
|
||||
export default class AppPermission extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -19,7 +20,7 @@ export default class AppPermission extends React.Component {
|
|||
|
||||
const searchParams = new URLSearchParams(wurl.search);
|
||||
|
||||
if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
|
||||
curl = url.parse(searchParams.get('url'));
|
||||
if (curl) {
|
||||
curl.search = curl.query = "";
|
||||
|
@ -33,25 +34,16 @@ export default class AppPermission extends React.Component {
|
|||
return curlString;
|
||||
}
|
||||
|
||||
isScalarWurl(wurl) {
|
||||
if (wurl && wurl.hostname && (
|
||||
wurl.hostname === 'scalar.vector.im' ||
|
||||
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||
wurl.hostname === 'demo.riot.im' ||
|
||||
wurl.hostname === 'localhost'
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
let e2eWarningText;
|
||||
if (this.props.isRoomEncrypted) {
|
||||
e2eWarningText =
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
||||
}
|
||||
const cookieWarning =
|
||||
<span className='mx_AppPermissionWarningTextLabel'>
|
||||
{ _t('Warning: This widget might use cookies.') }
|
||||
</span>;
|
||||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
|
@ -60,6 +52,7 @@ export default class AppPermission extends React.Component {
|
|||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||
{ e2eWarningText }
|
||||
{ cookieWarning }
|
||||
</div>
|
||||
<input
|
||||
className='mx_AppPermissionButton'
|
||||
|
|
|
@ -25,14 +25,13 @@ import PlatformPeg from '../../../PlatformPeg';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import MessageSpinner from './MessageSpinner';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
|
@ -55,6 +54,7 @@ export default class AppTile extends React.Component {
|
|||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
||||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,30 +120,6 @@ export default class AppTile extends React.Component {
|
|||
return u.format();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
||||
* @param {[type]} url URL to check
|
||||
* @return {Boolean} True if specified URL is a scalar URL
|
||||
*/
|
||||
isScalarUrl(url) {
|
||||
if (!url) {
|
||||
console.error('Scalar URL check failed. No URL specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||
if (!scalarUrls || scalarUrls.length == 0) {
|
||||
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||
}
|
||||
|
||||
for (let i = 0; i < scalarUrls.length; i++) {
|
||||
if (url.startsWith(scalarUrls[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.url);
|
||||
|
@ -199,7 +175,7 @@ export default class AppTile extends React.Component {
|
|||
setScalarToken() {
|
||||
this.setState({initialising: true});
|
||||
|
||||
if (!this.isScalarUrl(this.props.url)) {
|
||||
if (!WidgetUtils.isScalarUrl(this.props.url)) {
|
||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
|
@ -269,7 +245,12 @@ export default class AppTile extends React.Component {
|
|||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
if (!this.state.widgetUrl.startsWith(event.origin)) {
|
||||
const widgetUrlObj = url.parse(this.state.widgetUrl);
|
||||
const eventOrigin = url.parse(event.origin);
|
||||
if (
|
||||
eventOrigin.protocol !== widgetUrlObj.protocol ||
|
||||
eventOrigin.host !== widgetUrlObj.host
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -338,10 +319,9 @@ export default class AppTile extends React.Component {
|
|||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
|
@ -519,6 +499,11 @@ export default class AppTile extends React.Component {
|
|||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick(e) {
|
||||
// Reload iframe in this way to avoid cross-origin restrictions
|
||||
this.refs.appFrame.src = this.refs.appFrame.src;
|
||||
}
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
||||
|
@ -606,6 +591,7 @@ export default class AppTile extends React.Component {
|
|||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||
const popoutWidgetIcon = 'img/button-new-window.svg';
|
||||
const reloadWidgetIcon = 'img/button-refresh.svg';
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
return (
|
||||
|
@ -624,6 +610,16 @@ export default class AppTile extends React.Component {
|
|||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Reload widget */ }
|
||||
{ this.props.showReload && <TintableSvgButton
|
||||
src={reloadWidgetIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Reload widget')}
|
||||
onClick={this._onReloadWidgetClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Popout widget */ }
|
||||
{ this.props.showPopout && <TintableSvgButton
|
||||
src={popoutWidgetIcon}
|
||||
|
@ -707,6 +703,11 @@ AppTile.propTypes = {
|
|||
showDelete: PropTypes.bool,
|
||||
// Optionally hide the popout widget icon
|
||||
showPopout: PropTypes.bool,
|
||||
// Optionally show the reload widget icon
|
||||
// This is not currently intended for use with production widgets. However
|
||||
// it can be useful when developing persistent widgets in order to avoid
|
||||
// having to reload all of riot to get new widget content.
|
||||
showReload: PropTypes.bool,
|
||||
// Widget capabilities to allow by default (without user confirmation)
|
||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
|
@ -726,6 +727,7 @@ AppTile.defaultProps = {
|
|||
showMinimise: true,
|
||||
showDelete: true,
|
||||
showPopout: true,
|
||||
showReload: false,
|
||||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
userWidget: false,
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import TagTile from './TagTile';
|
||||
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
export default function DNDTagTile(props) {
|
||||
|
|
|
@ -29,6 +29,9 @@ module.exports = React.createClass({
|
|||
// The primary button which is styled differently and has default focus.
|
||||
primaryButton: PropTypes.node.isRequired,
|
||||
|
||||
// A node to insert into the cancel button instead of default "Cancel"
|
||||
cancelButton: PropTypes.node,
|
||||
|
||||
// onClick handler for the primary button.
|
||||
onPrimaryButtonClick: PropTypes.func.isRequired,
|
||||
|
||||
|
@ -60,9 +63,9 @@ module.exports = React.createClass({
|
|||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||
}
|
||||
let cancelButton;
|
||||
if (this.props.hasCancel) {
|
||||
if (this.props.cancelButton || this.props.hasCancel) {
|
||||
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
|
||||
{ _t("Cancel") }
|
||||
{ this.props.cancelButton || _t("Cancel") }
|
||||
</button>;
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -139,8 +139,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
{ editableItems }
|
||||
{ this.props.canEdit ?
|
||||
// This is slightly evil; we want a new instance of
|
||||
// EditableItem when the list grows. To make sure it's
|
||||
// reset to its initial state.
|
||||
<EditableItem
|
||||
key={-1}
|
||||
key={editableItems.length}
|
||||
initialValue={this.props.newItem}
|
||||
onAdd={this.onItemAdded}
|
||||
onChange={this.onNewItemChanged}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
|
@ -14,15 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
const KEY_WINDOWS = 91;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableText',
|
||||
|
||||
|
@ -66,9 +61,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.initialValue !== this.props.initialValue ||
|
||||
nextProps.initialValue !== this.value
|
||||
) {
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
this.showPlaceholder(!this.value);
|
||||
|
@ -139,7 +132,7 @@ module.exports = React.createClass({
|
|||
this.showPlaceholder(false);
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
if (ev.key === "Enter") {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
@ -156,9 +149,9 @@ module.exports = React.createClass({
|
|||
this.value = ev.target.textContent;
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
if (ev.key === "Enter") {
|
||||
this.onFinish(ev);
|
||||
} else if (ev.key == "Escape") {
|
||||
} else if (ev.key === "Escape") {
|
||||
this.cancelEdit();
|
||||
}
|
||||
|
||||
|
@ -193,7 +186,7 @@ module.exports = React.createClass({
|
|||
const submit = (ev.key === "Enter") || shouldSubmit;
|
||||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
}, () => {
|
||||
if (this.value !== this.props.initialValue) {
|
||||
self.onValueChanged(submit);
|
||||
}
|
||||
|
@ -204,23 +197,35 @@ module.exports = React.createClass({
|
|||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
if (this.props.blurToCancel) {this.cancelEdit();} else {this.onFinish(ev, this.props.blurToSubmit);}
|
||||
if (this.props.blurToCancel) {
|
||||
this.cancelEdit();
|
||||
} else {
|
||||
this.onFinish(ev, this.props.blurToSubmit);
|
||||
}
|
||||
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let editable_el;
|
||||
const {className, editable, initialValue, label, labelClassName} = this.props;
|
||||
let editableEl;
|
||||
|
||||
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
||||
if (!editable || (this.state.phase === this.Phases.Display && (label || labelClassName) && !this.value)) {
|
||||
// show the label
|
||||
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
|
||||
editableEl = <div className={className + " " + labelClassName} onClick={this.onClickDiv}>
|
||||
{ label || initialValue }
|
||||
</div>;
|
||||
} else {
|
||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||
editable_el = <div ref="editable_div" contentEditable="true" className={this.props.className}
|
||||
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
|
||||
editableEl = <div ref="editable_div"
|
||||
contentEditable={true}
|
||||
className={className}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur} />;
|
||||
}
|
||||
|
||||
return editable_el;
|
||||
return editableEl;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,28 +16,24 @@ limitations under the License.
|
|||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
const ContainerId = "mx_PersistedElement";
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(ContainerId);
|
||||
function getOrCreateContainer(containerId) {
|
||||
let container = document.getElementById(containerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = ContainerId;
|
||||
container.id = containerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// Greater than that of the ContextualMenu
|
||||
const PE_Z_INDEX = 3000;
|
||||
|
||||
/*
|
||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||
* in a container element appended to document.body.
|
||||
|
@ -50,6 +46,14 @@ const PE_Z_INDEX = 3000;
|
|||
* bounding rect as the parent of PE.
|
||||
*/
|
||||
export default class PersistedElement extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
// Unique identifier for this PersistedElement instance
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
||||
|
@ -97,18 +101,16 @@ export default class PersistedElement extends React.Component {
|
|||
left: parentRect.left + 'px',
|
||||
width: parentRect.width + 'px',
|
||||
height: parentRect.height + 'px',
|
||||
zIndex: PE_Z_INDEX,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = <div ref={this.collectChild}>
|
||||
const content = <div ref={this.collectChild} style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(content, getOrCreateContainer());
|
||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||
|
||||
return <div ref={this.collectChildContainer}></div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -22,12 +23,13 @@ import PropTypes from 'prop-types';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
|
||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+])[^\/]*)$/;
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^\/]*)$/;
|
||||
|
||||
const Pill = React.createClass({
|
||||
statics: {
|
||||
|
@ -45,6 +47,7 @@ const Pill = React.createClass({
|
|||
},
|
||||
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
||||
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
||||
TYPE_GROUP_MENTION: 'TYPE_GROUP_MENTION',
|
||||
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
|
||||
},
|
||||
|
||||
|
@ -83,12 +86,14 @@ const Pill = React.createClass({
|
|||
|
||||
// The member related to the user pill
|
||||
member: null,
|
||||
// The group related to the group pill
|
||||
group: null,
|
||||
// The room related to the room pill
|
||||
room: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
let regex = REGEX_MATRIXTO;
|
||||
if (nextProps.inMessage) {
|
||||
regex = REGEX_LOCAL_MATRIXTO;
|
||||
|
@ -111,9 +116,11 @@ const Pill = React.createClass({
|
|||
'@': Pill.TYPE_USER_MENTION,
|
||||
'#': Pill.TYPE_ROOM_MENTION,
|
||||
'!': Pill.TYPE_ROOM_MENTION,
|
||||
'+': Pill.TYPE_GROUP_MENTION,
|
||||
}[prefix];
|
||||
|
||||
let member;
|
||||
let group;
|
||||
let room;
|
||||
switch (pillType) {
|
||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||
|
@ -142,8 +149,21 @@ const Pill = React.createClass({
|
|||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
try {
|
||||
group = await FlairStore.getGroupProfileCached(cli, resourceId);
|
||||
} catch (e) { // if FlairStore failed, fall back to just groupId
|
||||
group = {
|
||||
groupId: resourceId,
|
||||
avatarUrl: null,
|
||||
name: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({resourceId, pillType, member, room});
|
||||
this.setState({resourceId, pillType, member, group, room});
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -181,6 +201,7 @@ const Pill = React.createClass({
|
|||
});
|
||||
},
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
|
@ -231,6 +252,20 @@ const Pill = React.createClass({
|
|||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
if (this.state.group) {
|
||||
const {avatarUrl, groupId, name} = this.state.group;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
linkText = groupId;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <BaseAvatar name={name || groupId} width={16} height={16}
|
||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
||||
}
|
||||
pillClass = 'mx_GroupPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const classes = classNames(pillClass, {
|
||||
|
|
|
@ -160,7 +160,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
static makeThread(parentEv, onWidgetLoad, ref) {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_rich_quoting") || !ReplyThread.getParentEventId(parentEv)) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div />;
|
||||
}
|
||||
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -103,14 +104,27 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
_openContextMenu: function(x, y, chevronOffset) {
|
||||
// Hide the (...) immediately
|
||||
this.setState({ hover: false });
|
||||
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
|
@ -119,17 +133,14 @@ export default React.createClass({
|
|||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
const self = this;
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: function() {
|
||||
self.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
this._openContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._openContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
|
@ -164,7 +175,7 @@ export default React.createClass({
|
|||
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <div />;
|
||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||
return <AccessibleButton className={className} onClick={this.onClick} onContextMenu={this.onContextMenu}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
|||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
||||
export default class CookieBar extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -29,6 +30,10 @@ export default class CookieBar extends React.Component {
|
|||
super();
|
||||
}
|
||||
|
||||
onUsageDataClicked() {
|
||||
Analytics.showDetailsModal();
|
||||
}
|
||||
|
||||
onAccept() {
|
||||
dis.dispatch({
|
||||
action: 'accept_cookies',
|
||||
|
@ -49,11 +54,18 @@ export default class CookieBar extends React.Component {
|
|||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ this.props.policyUrl ? _t(
|
||||
"Help improve Riot by sending usage data? " +
|
||||
"This will use a cookie. " +
|
||||
"(See our <PolicyLink>cookie and privacy policies</PolicyLink>).",
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
"This will use a cookie " +
|
||||
"(please see our <PolicyLink>Cookie Policy</PolicyLink>).",
|
||||
{},
|
||||
{
|
||||
'UsageDataLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
href="javascript:;"
|
||||
onClick={this.onUsageDataClicked}
|
||||
>
|
||||
{ sub }
|
||||
</a>,
|
||||
// XXX: We need to link to the page that explains our cookies
|
||||
'PolicyLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
|
@ -64,10 +76,23 @@ export default class CookieBar extends React.Component {
|
|||
</a>
|
||||
,
|
||||
},
|
||||
) : _t("Help improve Riot by sending usage data? This will use a cookie.") }
|
||||
) : _t(
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
"This will use a cookie.",
|
||||
{},
|
||||
{
|
||||
'UsageDataLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
href="javascript:;"
|
||||
onClick={this.onUsageDataClicked}
|
||||
>
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
|
||||
{ _t("Yes please") }
|
||||
{ _t("Yes, I want to help!") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
|
||||
<img src="img/cancel.svg" width="18" height="18" />
|
||||
|
|
|
@ -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.
|
||||
|
@ -14,28 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
onUpdateClicked: function() {
|
||||
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog, {
|
||||
onFinished: (passwordChanged) => {
|
||||
if (!passwordChanged) {
|
||||
return;
|
||||
}
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({
|
||||
action: 'password_changed',
|
||||
});
|
||||
},
|
||||
});
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,6 +21,9 @@ import { MatrixClient } from 'matrix-js-sdk';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {createMenu} from "../../structures/ContextualMenu";
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupInviteTile',
|
||||
|
@ -32,6 +36,15 @@ export default React.createClass({
|
|||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return ({
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
menuDisplayed: false,
|
||||
selected: this.props.group.groupId === null, // XXX: this needs linking to LoggedInView/GroupView state
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
|
@ -39,6 +52,69 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMouseEnter: function() {
|
||||
const state = {hover: true};
|
||||
// Only allow non-guests to access the context menu
|
||||
if (!this.context.matrixClient.isGuest()) {
|
||||
state.badgeHover = true;
|
||||
}
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
onMouseLeave: function() {
|
||||
this.setState({
|
||||
badgeHover: false,
|
||||
hover: false,
|
||||
});
|
||||
},
|
||||
|
||||
_showContextMenu: function(x, y, chevronOffset) {
|
||||
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
|
||||
|
||||
createMenu(GroupInviteTileContextMenu, {
|
||||
chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
group: this.props.group,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.preventDefault();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
onBadgeClicked: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
this._showContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
@ -49,19 +125,40 @@ export default React.createClass({
|
|||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||
|
||||
const label = <EmojiText
|
||||
element="div"
|
||||
title={this.props.group.groupId}
|
||||
className="mx_RoomTile_name mx_RoomTile_badgeShown"
|
||||
dir="auto"
|
||||
>
|
||||
const nameClasses = classNames('mx_RoomTile_name mx_RoomTile_invite mx_RoomTile_badgeShown', {
|
||||
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||
{ groupName }
|
||||
</EmojiText>;
|
||||
|
||||
const badge = <div className="mx_RoomSubList_badge mx_RoomSubList_badgeHighlight">!</div>;
|
||||
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_badgeButton': badgeEllipsis,
|
||||
});
|
||||
|
||||
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
||||
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
|
||||
let tooltip;
|
||||
if (this.props.collapsed && this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
|
||||
}
|
||||
|
||||
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomTile mx_RoomTile_highlight" onClick={this.onClick}>
|
||||
<AccessibleButton className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
|
@ -69,6 +166,7 @@ export default React.createClass({
|
|||
{ label }
|
||||
{ badge }
|
||||
</div>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -187,7 +187,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
|
|
|
@ -69,7 +69,7 @@ export default React.createClass({
|
|||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
const input = <input type="checkbox"
|
||||
onClick={this._onPublicityToggle}
|
||||
onChange={this._onPublicityToggle}
|
||||
checked={this.state.isGroupPublicised}
|
||||
/>;
|
||||
const labelText = !this.state.ready ? _t("Loading...") :
|
||||
|
|
|
@ -22,6 +22,7 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
function nop() {}
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
@ -81,7 +82,7 @@ const GroupTile = React.createClass({
|
|||
) : null;
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown}>
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
|
|
|
@ -28,6 +28,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
*/
|
||||
class PasswordLogin extends React.Component {
|
||||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
|
@ -56,33 +57,64 @@ class PasswordLogin extends React.Component {
|
|||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
||||
this.isLoginEmpty = this.isLoginEmpty.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._passwordField = null;
|
||||
this._loginField = null;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
||||
field_input_incorrect(this._passwordField);
|
||||
field_input_incorrect(this.isLoginEmpty() ? this._loginField : this._passwordField);
|
||||
}
|
||||
}
|
||||
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
|
||||
this.props.onSubmit(
|
||||
'', // XXX: Synapse breaks if you send null here:
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
|
||||
let username = ''; // XXX: Synapse breaks if you send null here:
|
||||
let phoneCountry = null;
|
||||
let phoneNumber = null;
|
||||
let error;
|
||||
|
||||
switch (this.state.loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The email field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The user name field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
phoneCountry = this.state.phoneCountry;
|
||||
phoneNumber = this.state.phoneNumber;
|
||||
if (!phoneNumber) {
|
||||
error = _t('The phone number field must not be blank.');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.props.onError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.password) {
|
||||
this.props.onError(_t('The password field must not be blank.'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
null,
|
||||
null,
|
||||
username,
|
||||
phoneCountry,
|
||||
phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
}
|
||||
|
@ -93,6 +125,7 @@ class PasswordLogin extends React.Component {
|
|||
}
|
||||
|
||||
onLoginTypeChange(loginType) {
|
||||
this.props.onError(null); // send a null error to clear any error messages
|
||||
this.setState({
|
||||
loginType: loginType,
|
||||
username: "", // Reset because email and username use the same state
|
||||
|
@ -126,8 +159,10 @@ class PasswordLogin extends React.Component {
|
|||
switch (loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
classes.mx_Login_email = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <input
|
||||
className={classNames(classes)}
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="email_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
|
@ -139,8 +174,10 @@ class PasswordLogin extends React.Component {
|
|||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
classes.mx_Login_username = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <input
|
||||
className={classNames(classes)}
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="username_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
|
@ -153,14 +190,14 @@ class PasswordLogin extends React.Component {
|
|||
autoFocus
|
||||
disabled={disabled}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
classes.mx_Login_phoneNumberField = true;
|
||||
classes.mx_Login_field_has_prefix = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
|
||||
return <div className="mx_Login_phoneSection">
|
||||
<CountryDropdown
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
ref="phone_country"
|
||||
onOptionChange={this.onPhoneCountryChanged}
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
|
@ -169,7 +206,7 @@ class PasswordLogin extends React.Component {
|
|||
/>
|
||||
<input
|
||||
className={classNames(classes)}
|
||||
ref="phoneNumber"
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="phone_input"
|
||||
type="text"
|
||||
name="phoneNumber"
|
||||
|
@ -180,6 +217,17 @@ class PasswordLogin extends React.Component {
|
|||
disabled={disabled}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isLoginEmpty() {
|
||||
switch (this.state.loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
return !this.state.username;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
return !this.state.phoneCountry || !this.state.phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,7 +255,7 @@ class PasswordLogin extends React.Component {
|
|||
const pwFieldClass = classNames({
|
||||
mx_Login_field: true,
|
||||
mx_Login_field_disabled: matrixIdText === '',
|
||||
error: this.props.loginIncorrect,
|
||||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
@ -258,6 +306,7 @@ PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
|||
|
||||
PasswordLogin.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onError: PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
|
|
|
@ -327,6 +327,7 @@ module.exports = React.createClass({
|
|||
// will have the correct name when the user tries to download it.
|
||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
rel: "noopener",
|
||||
target: "_blank",
|
||||
textContent: _t("Download %(text)s", { text: text }),
|
||||
}, "*");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,48 +16,43 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import ImageUtils from '../../../ImageUtils';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class extends React.Component {
|
||||
displayName: 'MImageBody'
|
||||
|
||||
export default class MImageBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* called when the image has loaded */
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
/* the maximum image height to use */
|
||||
maxImageHeight: PropTypes.number,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onImageError = this.onImageError.bind(this);
|
||||
this.onImageLoad = this.onImageLoad.bind(this);
|
||||
this.onImageEnter = this.onImageEnter.bind(this);
|
||||
this.onImageLeave = this.onImageLeave.bind(this);
|
||||
this.onClientSync = this.onClientSync.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.fixupHeight = this.fixupHeight.bind(this);
|
||||
this._isGif = this._isGif.bind(this);
|
||||
|
||||
this.state = {
|
||||
|
@ -65,6 +61,9 @@ export default class extends React.Component {
|
|||
decryptedBlob: null,
|
||||
error: null,
|
||||
imgError: false,
|
||||
imgLoaded: false,
|
||||
loadedImageDimensions: null,
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -88,7 +87,7 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onClick(ev) {
|
||||
if (ev.button == 0 && !ev.metaKey) {
|
||||
if (ev.button === 0 && !ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const httpUrl = this._getContentUrl();
|
||||
|
@ -119,6 +118,8 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onImageEnter(e) {
|
||||
this.setState({ hover: true });
|
||||
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
|
@ -127,6 +128,8 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onImageLeave(e) {
|
||||
this.setState({ hover: false });
|
||||
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
|
@ -142,6 +145,16 @@ export default class extends React.Component {
|
|||
|
||||
onImageLoad() {
|
||||
this.props.onWidgetLoad();
|
||||
|
||||
let loadedImageDimensions;
|
||||
|
||||
if (this.refs.image) {
|
||||
const { naturalWidth, naturalHeight } = this.refs.image;
|
||||
|
||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||
}
|
||||
|
||||
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||
}
|
||||
|
||||
_getContentUrl() {
|
||||
|
@ -161,9 +174,7 @@ export default class extends React.Component {
|
|||
return this.state.decryptedThumbnailUrl;
|
||||
}
|
||||
return this.state.decryptedUrl;
|
||||
} else if (content.info &&
|
||||
content.info.mimetype == "image/svg+xml" &&
|
||||
content.info.thumbnail_url) {
|
||||
} else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) {
|
||||
// special case to return client-generated thumbnails for SVGs, if any,
|
||||
// given we deliberately don't thumbnail them serverside to prevent
|
||||
// billion lol attacks and similar
|
||||
|
@ -176,11 +187,10 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
if (content.info && content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file,
|
||||
).then(function(blob) {
|
||||
|
@ -207,7 +217,6 @@ export default class extends React.Component {
|
|||
});
|
||||
}).done();
|
||||
}
|
||||
this.fixupHeight();
|
||||
this._afterComponentDidMount();
|
||||
}
|
||||
|
||||
|
@ -218,7 +227,6 @@ export default class extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
this._afterComponentWillUnmount();
|
||||
|
||||
|
@ -235,59 +243,113 @@ export default class extends React.Component {
|
|||
_afterComponentWillUnmount() {
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
if (payload.action === "timeline_resize") {
|
||||
this.fixupHeight();
|
||||
}
|
||||
}
|
||||
|
||||
fixupHeight() {
|
||||
if (!this.refs.image) {
|
||||
console.warn(`Refusing to fix up height on ${this.displayName} with no image element`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const timelineWidth = this.refs.body.offsetWidth;
|
||||
const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
|
||||
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
|
||||
|
||||
// FIXME: this will break on clientside generated thumbnails (as per e2e rooms)
|
||||
// which may well be much smaller than the 800x600 bounding box.
|
||||
|
||||
// FIXME: It will also break really badly for images with broken or missing thumbnails
|
||||
|
||||
// FIXME: Because we don't know what size of thumbnail the server's actually going to send
|
||||
// us, we can't even really layout the page nicely for it. Instead we have to assume
|
||||
// it'll target 800x600 and we'll downsize if needed to make things fit.
|
||||
|
||||
// console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
|
||||
let thumbHeight = null;
|
||||
if (content.info) {
|
||||
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
|
||||
}
|
||||
this.refs.image.style.height = thumbHeight + "px";
|
||||
// console.log("Image height now", thumbHeight);
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
let infoWidth;
|
||||
let infoHeight;
|
||||
|
||||
if (content && content.info && content.info.w && content.info.h) {
|
||||
infoWidth = content.info.w;
|
||||
infoHeight = content.info.h;
|
||||
} else {
|
||||
// Whilst the image loads, display nothing.
|
||||
//
|
||||
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
||||
//
|
||||
// By doing this, the image "pops" into the timeline, but is still restricted
|
||||
// by the same width and height logic below.
|
||||
if (!this.state.loadedImageDimensions) {
|
||||
return this.wrapImage(contentUrl,
|
||||
<img style={{display: 'none'}} src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
infoWidth = this.state.loadedImageDimensions.naturalWidth;
|
||||
infoHeight = this.state.loadedImageDimensions.naturalHeight;
|
||||
}
|
||||
|
||||
// The maximum height of the thumbnail as it is rendered as an <img>
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
|
||||
// The maximum width of the thumbnail, as dictated by its natural
|
||||
// maximum height.
|
||||
const maxWidth = infoWidth * maxHeight / infoHeight;
|
||||
|
||||
let img = null;
|
||||
let placeholder = null;
|
||||
|
||||
// e2e image hasn't been decrypted yet
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
placeholder = <img src="img/spinner.gif" alt={content.body} width="32" height="32" />;
|
||||
} else if (!this.state.imgLoaded) {
|
||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
||||
placeholder = this.getPlaceholder();
|
||||
}
|
||||
|
||||
const showPlaceholder = Boolean(placeholder);
|
||||
|
||||
if (thumbUrl && !this.state.imgError) {
|
||||
// Restrict the width of the thumbnail here, otherwise it will fill the container
|
||||
// which has the same width as the timeline
|
||||
// mx_MImageBody_thumbnail resizes img to exactly container size
|
||||
img = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />;
|
||||
}
|
||||
|
||||
const thumbnail = (
|
||||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px" }} >
|
||||
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
|
||||
<div style={{ paddingBottom: (100 * infoHeight / infoWidth) + '%' }} />
|
||||
|
||||
{ showPlaceholder &&
|
||||
<div className="mx_MImageBody_thumbnail" style={{
|
||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
maxWidth: infoWidth + "px",
|
||||
}}>
|
||||
<div className="mx_MImageBody_thumbnail_spinner">
|
||||
{ placeholder }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style={{display: !showPlaceholder ? undefined : 'none'}}>
|
||||
{ img }
|
||||
</div>
|
||||
|
||||
{ this.state.hover && this.getTooltip() }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
{ thumbUrl && !this.state.imgError ? thumbnail : '' }
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
return this.wrapImage(contentUrl, thumbnail);
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
wrapImage(contentUrl, children) {
|
||||
return <a href={contentUrl} onClick={this.onClick}>
|
||||
{children}
|
||||
</a>;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getPlaceholder() {
|
||||
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getTooltip() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getFileBody() {
|
||||
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -302,25 +364,6 @@ export default class extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||
"display": "flex",
|
||||
"alignItems": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
|
||||
"margin": "auto",
|
||||
}} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
|
@ -329,6 +372,12 @@ export default class extends React.Component {
|
|||
thumbUrl = this._getThumbUrl();
|
||||
}
|
||||
|
||||
return this._messageContent(contentUrl, thumbUrl, content);
|
||||
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
|
||||
const fileBody = this.getFileBody();
|
||||
|
||||
return <span className="mx_MImageBody" ref="body">
|
||||
{ thumbnail }
|
||||
{ fileBody }
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,145 +16,42 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MImageBody from './MImageBody';
|
||||
import sdk from '../../../index';
|
||||
import TintableSVG from '../elements/TintableSvg';
|
||||
|
||||
export default class MStickerBody extends MImageBody {
|
||||
displayName: 'MStickerBody'
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._onImageLoad = this._onImageLoad.bind(this);
|
||||
}
|
||||
|
||||
_onMouseEnter() {
|
||||
this.setState({showTooltip: true});
|
||||
}
|
||||
|
||||
_onMouseLeave() {
|
||||
this.setState({showTooltip: false});
|
||||
}
|
||||
|
||||
_onImageLoad() {
|
||||
this.setState({
|
||||
placeholderClasses: 'mx_MStickerBody_placeholder_invisible',
|
||||
});
|
||||
const hidePlaceholderTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
}, 500);
|
||||
this.setState({hidePlaceholderTimer});
|
||||
if (this.props.onWidgetLoad) {
|
||||
this.props.onWidgetLoad();
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentDidMount() {
|
||||
if (this.refs.image.complete) {
|
||||
// Image already loaded
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
placeholderClasses: '.mx_MStickerBody_placeholder_invisible',
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
} else {
|
||||
// Image not already loaded
|
||||
this.setState({
|
||||
placeholderVisible: true,
|
||||
placeholderClasses: '',
|
||||
imageClasses: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentWillUnmount() {
|
||||
if (this.state.hidePlaceholderTimer) {
|
||||
clearTimeout(this.state.hidePlaceholderTimer);
|
||||
this.setState({hidePlaceholderTimer: null});
|
||||
}
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
let tooltip;
|
||||
const tooltipBody = (
|
||||
this.props.mxEvent &&
|
||||
this.props.mxEvent.getContent() &&
|
||||
this.props.mxEvent.getContent().body) ?
|
||||
this.props.mxEvent.getContent().body : null;
|
||||
if (this.state.showTooltip && tooltipBody) {
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
tooltip = <RoomTooltip
|
||||
className='mx_RoleButton_tooltip'
|
||||
label={tooltipBody} />;
|
||||
}
|
||||
|
||||
const gutterSize = 0;
|
||||
let placeholderSize = 75;
|
||||
let placeholderFixupHeight = '100px';
|
||||
let placeholderTop = 0;
|
||||
let placeholderLeft = 0;
|
||||
|
||||
if (content.info) {
|
||||
placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px';
|
||||
placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px';
|
||||
placeholderFixupHeight = content.info.h + 'px';
|
||||
}
|
||||
|
||||
// The pixel size of sticker images is generally larger than their intended display
|
||||
// size so they render at native reolution on HiDPI displays. We therefore need to
|
||||
// explicity set the size so they render at the intended size.
|
||||
// XXX: This will be clobberred when we run fixupHeight(), but we need to do it
|
||||
// here otherwise the stickers are momentarily displayed at the pixel size.
|
||||
const imageStyle = {
|
||||
height: content.info.h,
|
||||
// leave the browser the calculate the width automatically
|
||||
};
|
||||
|
||||
placeholderSize = placeholderSize + 'px';
|
||||
|
||||
// Body 'ref' required by MImageBody
|
||||
return (
|
||||
<span className='mx_MStickerBody' ref='body'
|
||||
style={{
|
||||
height: placeholderFixupHeight,
|
||||
}}>
|
||||
<div className={'mx_MStickerBody_image_container'}>
|
||||
{ this.state.placeholderVisible &&
|
||||
<div
|
||||
className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses}
|
||||
style={{
|
||||
top: placeholderTop,
|
||||
left: placeholderLeft,
|
||||
}}
|
||||
>
|
||||
<TintableSVG
|
||||
src={'img/icons-show-stickers.svg'}
|
||||
width={placeholderSize}
|
||||
height={placeholderSize} />
|
||||
</div> }
|
||||
<img
|
||||
className={'mx_MStickerBody_image ' + this.state.imageClasses}
|
||||
src={contentUrl}
|
||||
style={imageStyle}
|
||||
ref='image'
|
||||
alt={content.body}
|
||||
onLoad={this._onImageLoad}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
/>
|
||||
{ tooltip }
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty to prevent default behaviour of MImageBody
|
||||
onClick() {
|
||||
}
|
||||
|
||||
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
|
||||
// which is added by mx_MStickerBody_wrapper
|
||||
wrapImage(contentUrl, children) {
|
||||
return <div className="mx_MStickerBody_wrapper"> { children } </div>;
|
||||
}
|
||||
|
||||
// Placeholder to show in place of the sticker image if
|
||||
// img onLoad hasn't fired yet.
|
||||
getPlaceholder() {
|
||||
const TintableSVG = sdk.getComponent('elements.TintableSvg');
|
||||
return <TintableSVG src="img/icons-show-stickers.svg" width="75" height="75" />;
|
||||
}
|
||||
|
||||
// Tooltip to show on mouse over
|
||||
getTooltip() {
|
||||
const content = this.props.mxEvent && this.props.mxEvent.getContent();
|
||||
|
||||
if (!content || !content.body || !content.info || !content.info.w) return null;
|
||||
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip">
|
||||
<RoomTooltip label={content.body} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// Don't show "Download this_file.png ..."
|
||||
getFileBody() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,12 +147,7 @@ module.exports = React.createClass({
|
|||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||
"display": "flex",
|
||||
"align-items": "center",
|
||||
"justify-items": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16" />
|
||||
</div>
|
||||
</span>
|
||||
|
|
|
@ -39,8 +39,11 @@ module.exports = React.createClass({
|
|||
/* callback called when dynamic content in events are loaded */
|
||||
onWidgetLoad: PropTypes.func,
|
||||
|
||||
/* the shsape of the tile, used */
|
||||
/* the shape of the tile, used */
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
/* the maximum image height to use, if the event is an image */
|
||||
maxImageHeight: PropTypes.number,
|
||||
},
|
||||
|
||||
getEventTileOps: function() {
|
||||
|
@ -59,17 +62,24 @@ module.exports = React.createClass({
|
|||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||
};
|
||||
const evTypes = {
|
||||
'm.sticker': sdk.getComponent('messages.MStickerBody'),
|
||||
};
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const type = this.props.mxEvent.getType();
|
||||
const msgtype = content.msgtype;
|
||||
let BodyType = UnknownBody;
|
||||
if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (this.props.mxEvent.getType() === 'm.sticker') {
|
||||
BodyType = sdk.getComponent('messages.MStickerBody');
|
||||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
if (!this.props.mxEvent.isRedacted()) {
|
||||
// only resolve BodyType if event is not redacted
|
||||
if (type && evTypes[type]) {
|
||||
BodyType = evTypes[type];
|
||||
} else if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
}
|
||||
}
|
||||
|
||||
return <BodyType
|
||||
|
@ -78,6 +88,7 @@ module.exports = React.createClass({
|
|||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
maxImageHeight={this.props.maxImageHeight}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -72,14 +72,12 @@ export default React.createClass({
|
|||
|
||||
_updateRelatedGroups() {
|
||||
if (this.unmounted) return;
|
||||
const relatedGroupsEvent = this.context.matrixClient
|
||||
.getRoom(this.props.mxEvent.getRoomId())
|
||||
.currentState
|
||||
.getStateEvents('m.room.related_groups', '');
|
||||
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||
this.setState({
|
||||
relatedGroups: relatedGroupsEvent ?
|
||||
relatedGroupsEvent.getContent().groups || []
|
||||
: [],
|
||||
relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import * as ContextualMenu from '../../structures/ContextualMenu';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {host as matrixtoHost} from '../../../matrix-to';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -304,7 +305,7 @@ module.exports = React.createClass({
|
|||
// never preview matrix.to links (if anything we should give a smart
|
||||
// preview of the room/user they point to: nobody needs to be reminded
|
||||
// what the matrix.to site looks like).
|
||||
if (host == 'matrix.to') return false;
|
||||
if (host === matrixtoHost) return false;
|
||||
|
||||
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
|
||||
// it's a "foo.pl" style link
|
||||
|
@ -336,10 +337,21 @@ module.exports = React.createClass({
|
|||
left: x,
|
||||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
e.target.onmouseout = close;
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
};
|
||||
p.appendChild(button);
|
||||
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
p.parentNode.replaceChild(div, p);
|
||||
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(p);
|
||||
div.appendChild(button);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -422,8 +434,7 @@ module.exports = React.createClass({
|
|||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
||||
const stripReply = SettingsStore.isFeatureEnabled("feature_rich_quoting") &&
|
||||
ReplyThread.getParentEventId(mxEvent);
|
||||
const stripReply = ReplyThread.getParentEventId(mxEvent);
|
||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||
// Part of Replies fallback support
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
|
@ -15,6 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const sdk = require("../../../index");
|
||||
|
@ -29,6 +31,10 @@ module.exports = React.createClass({
|
|||
room: PropTypes.object,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
const promises = [];
|
||||
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
||||
|
@ -39,42 +45,58 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
const roomId = this.props.room.roomId;
|
||||
const isEncrypted = this.context.matrixClient.isRoomEncrypted(roomId);
|
||||
|
||||
let previewsForAccount = null;
|
||||
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
}
|
||||
|
||||
let previewsForRoom = null;
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={this.props.room.roomId}
|
||||
isExplicit={true}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsRoom" />
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
|
||||
if (!isEncrypted) {
|
||||
// Only show account setting state and room state setting state in non-e2ee rooms where they apply
|
||||
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
|
||||
if (accountEnabled) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
||||
'a': (sub)=><a href="#/settings">{ sub }</a>,
|
||||
})
|
||||
);
|
||||
} else if (accountEnabled) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
||||
'a': (sub)=><a href="#/settings">{ sub }</a>,
|
||||
})
|
||||
);
|
||||
}
|
||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={roomId}
|
||||
isExplicit={true}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsRoom" />
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
}
|
||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||
}
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_t("In encrypted rooms, like this one, URL previews are disabled by default to ensure that your " +
|
||||
"homeserver (where the previews are generated) cannot gather information about links you see in " +
|
||||
"this room.")
|
||||
);
|
||||
}
|
||||
|
||||
const previewsForRoomAccount = (
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
|
||||
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
|
||||
level={SettingLevel.ROOM_ACCOUNT}
|
||||
roomId={this.props.room.roomId}
|
||||
roomId={roomId}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsSelf"
|
||||
/>
|
||||
|
@ -83,8 +105,13 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>{ _t("URL Previews") }</h3>
|
||||
|
||||
<label>{ previewsForAccount }</label>
|
||||
<div>
|
||||
{ _t('When someone puts a URL in their message, a URL preview can be shown to give more ' +
|
||||
'information about that link such as the title, description, and an image from the website.') }
|
||||
</div>
|
||||
<div>
|
||||
{ previewsForAccount }
|
||||
</div>
|
||||
{ previewsForRoom }
|
||||
<label>{ previewsForRoomAccount }</label>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
|
@ -94,15 +94,7 @@ module.exports = React.createClass({
|
|||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
// When opening the app drawer when there aren't any apps,
|
||||
// auto-launch the integrations manager to skip the awkward
|
||||
// click on "Add widget"
|
||||
if (action.show) {
|
||||
const apps = this._getApps();
|
||||
if (apps.length === 0) {
|
||||
this._launchManageIntegrations();
|
||||
}
|
||||
|
||||
localStorage.removeItem(hideWidgetKey);
|
||||
} else {
|
||||
// Store hidden state of widget
|
||||
|
@ -171,14 +163,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_getApps: function() {
|
||||
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
if (!appsStateEvents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return appsStateEvents.filter((ev) => {
|
||||
return ev.getContent().type && ev.getContent().url;
|
||||
}).map((ev) => {
|
||||
return WidgetUtils.getRoomWidgets(this.props.room).map((ev) => {
|
||||
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -34,6 +34,7 @@ const ContextualMenu = require('../../structures/ContextualMenu');
|
|||
import dis from '../../../dispatcher';
|
||||
import {makeEventPermalink} from "../../../matrix-to";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
|
||||
|
@ -55,6 +56,7 @@ const stateEventTileTypes = {
|
|||
'm.room.topic': 'messages.TextualEvent',
|
||||
'm.room.power_levels': 'messages.TextualEvent',
|
||||
'm.room.pinned_events': 'messages.TextualEvent',
|
||||
'm.room.server_acl' : 'messages.TextualEvent',
|
||||
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
};
|
||||
|
@ -442,26 +444,27 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const ev = this.props.mxEvent;
|
||||
const props = {onClick: this.onCryptoClicked};
|
||||
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||
return <E2ePadlockUndecryptable {...props} />;
|
||||
} else if (ev.isEncrypted()) {
|
||||
if (this.state.verified) {
|
||||
return <E2ePadlockVerified {...props} />;
|
||||
} else {
|
||||
return <E2ePadlockUnverified {...props} />;
|
||||
}
|
||||
} else {
|
||||
// XXX: if the event is being encrypted (ie eventSendStatus ===
|
||||
// encrypting), it might be nice to show something other than the
|
||||
// open padlock?
|
||||
}
|
||||
|
||||
// if the event is not encrypted, but it's an e2e room, show the
|
||||
// open padlock
|
||||
const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId());
|
||||
if (e2eEnabled) {
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
if (ev.isEncrypted()) {
|
||||
return this.state.verified ? <E2ePadlockVerified {...props} /> : <E2ePadlockUnverified {...props} />;
|
||||
}
|
||||
|
||||
if (this.props.matrixClient.isRoomEncrypted(ev.getRoomId())) {
|
||||
// else if room is encrypted
|
||||
// and event is being encrypted or is not_sent (Unknown Devices/Network Error)
|
||||
if (ev.status === EventStatus.ENCRYPTING) {
|
||||
return <E2ePadlockEncrypting {...props} />;
|
||||
}
|
||||
if (ev.status === EventStatus.NOT_SENT) {
|
||||
return <E2ePadlockNotSent {...props} />;
|
||||
}
|
||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
}
|
||||
|
||||
// no padlock needed
|
||||
|
@ -490,7 +493,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -608,13 +611,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</EmojiText>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
|
@ -715,9 +719,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
}));
|
||||
|
||||
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
||||
const messageTypes = ['m.room.message', 'm.sticker'];
|
||||
function isMessageEvent(ev) {
|
||||
return (messageTypes.includes(ev.getType()));
|
||||
}
|
||||
|
||||
module.exports.haveTileForEvent = function(e) {
|
||||
// Only messages have a tile (black-rectangle) if redacted
|
||||
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
|
||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||
|
||||
const handler = getHandlerTile(e);
|
||||
if (handler === undefined) return false;
|
||||
|
@ -736,6 +746,14 @@ function E2ePadlockUndecryptable(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function E2ePadlockEncrypting(props) {
|
||||
return <E2ePadlock alt={_t("Encrypting")} src="img/e2e-encrypting.svg" width="10" height="12" {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockNotSent(props) {
|
||||
return <E2ePadlock alt={_t("Encrypted, not sent")} src="img/e2e-not_sent.svg" width="10" height="12" {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockVerified(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Encrypted by a verified device")}
|
||||
|
|
|
@ -332,13 +332,40 @@ module.exports = withMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMuteToggle: function() {
|
||||
_warnSelfDemote: function() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
return new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Demote yourself?"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the room it will be impossible " +
|
||||
"to regain privileges.") }
|
||||
</div>,
|
||||
button: _t("Demote"),
|
||||
onFinished: resolve,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onMuteToggle: async function() {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
// if muting self, warn as it may be irreversible
|
||||
if (target === this.props.matrixClient.getUserId()) {
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
|
@ -436,7 +463,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
onPowerChange: function(powerLevel) {
|
||||
onPowerChange: async function(powerLevel) {
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
|
@ -455,20 +482,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
if (myUserId === target) {
|
||||
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
}
|
||||
},
|
||||
});
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -478,7 +497,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
|
||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||
"to have the same power level as yourself.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
|
@ -632,6 +652,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
onShareUserClick: function() {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
|
||||
target: this.props.member,
|
||||
});
|
||||
},
|
||||
|
||||
_renderUserOptions: function() {
|
||||
const cli = this.props.matrixClient;
|
||||
const member = this.props.member;
|
||||
|
@ -705,13 +732,18 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
if (!ignoreButton && !readReceiptButton && !insertPillButton && !inviteUserButton) return null;
|
||||
const shareUserButton = (
|
||||
<AccessibleButton onClick={this.onShareUserClick} className="mx_MemberInfo_field">
|
||||
{ _t('Share Link to User') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("User Options") }</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{ readReceiptButton }
|
||||
{ shareUserButton }
|
||||
{ insertPillButton }
|
||||
{ ignoreButton }
|
||||
{ inviteUserButton }
|
||||
|
@ -902,7 +934,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
|
|
@ -270,7 +270,7 @@ module.exports = React.createClass({
|
|||
|
||||
// console.log("comparing " + this.memberString(memberA) + " and " + this.memberString(memberB));
|
||||
|
||||
if (userA.currentlyActive && userB.currentlyActive) {
|
||||
if ((userA.currentlyActive && userB.currentlyActive) || !this._showPresence) {
|
||||
// console.log(memberA.name + " and " + memberB.name + " are both active");
|
||||
if (memberA.powerLevel === memberB.powerLevel) {
|
||||
// console.log(memberA + " and " + memberB + " have same power level");
|
||||
|
|
|
@ -155,54 +155,20 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
// _startCallApp(isAudioConf) {
|
||||
// dis.dispatch({
|
||||
// action: 'appsDrawer',
|
||||
// show: true,
|
||||
// });
|
||||
|
||||
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
// let appsStateEvent = {};
|
||||
// if (appsStateEvents) {
|
||||
// appsStateEvent = appsStateEvents.getContent();
|
||||
// }
|
||||
// if (!appsStateEvent.videoConf) {
|
||||
// appsStateEvent.videoConf = {
|
||||
// type: 'jitsi',
|
||||
// // FIXME -- This should not be localhost
|
||||
// url: 'http://localhost:8000/jitsi.html',
|
||||
// data: {
|
||||
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
||||
// isAudioConf: isAudioConf,
|
||||
// },
|
||||
// };
|
||||
// MatrixClientPeg.get().sendStateEvent(
|
||||
// this.props.room.roomId,
|
||||
// 'im.vector.modular.widgets',
|
||||
// appsStateEvent,
|
||||
// '',
|
||||
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
||||
// }
|
||||
// }
|
||||
|
||||
onCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(false);
|
||||
}
|
||||
|
||||
onVoiceCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: "voice",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(true);
|
||||
}
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 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.
|
||||
|
@ -37,7 +37,7 @@ import Promise from 'bluebird';
|
|||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||
import SlashCommands from '../../../SlashCommands';
|
||||
import {processCommandInput} from '../../../SlashCommands';
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
|
@ -54,8 +54,7 @@ import Markdown from '../../../Markdown';
|
|||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||
|
||||
import {MATRIXTO_URL_PATTERN, MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix';
|
||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||
import {MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix';
|
||||
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
|
||||
|
||||
import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort, toShort} from 'emojione';
|
||||
|
@ -314,7 +313,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
switch (payload.action) {
|
||||
case 'reply_to_event':
|
||||
case 'focus_composer':
|
||||
editor.focus();
|
||||
this.focusComposer();
|
||||
break;
|
||||
case 'insert_mention':
|
||||
{
|
||||
|
@ -1511,6 +1510,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
focusComposer = () => {
|
||||
this.refs.editor.focus();
|
||||
};
|
||||
|
||||
render() {
|
||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||
|
||||
|
@ -1519,9 +1522,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer_input_wrapper">
|
||||
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
|
||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <ReplyPreview /> }
|
||||
<ReplyPreview />
|
||||
<Autocomplete
|
||||
ref={(e) => this.autocomplete = e}
|
||||
room={this.props.room}
|
||||
|
|
|
@ -22,6 +22,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import MessageEvent from "../messages/MessageEvent";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {formatFullDate} from '../../../DateUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PinnedEventTile',
|
||||
|
@ -80,11 +81,20 @@ module.exports = React.createClass({
|
|||
{ unpinButton }
|
||||
</div>
|
||||
|
||||
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
|
||||
<span className="mx_PinnedEventTile_senderAvatar">
|
||||
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
|
||||
</span>
|
||||
<span className="mx_PinnedEventTile_sender">
|
||||
{ sender.name }
|
||||
</span>
|
||||
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" />
|
||||
<span className="mx_PinnedEventTile_timestamp">
|
||||
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
|
||||
</span>
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150}
|
||||
onWidgetLoad={() => {}} // we need to give this, apparently
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -39,6 +39,19 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._updatePinnedMessages();
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
|
||||
}
|
||||
},
|
||||
|
||||
_onStateEvent: function(ev) {
|
||||
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
|
||||
this._updatePinnedMessages();
|
||||
}
|
||||
},
|
||||
|
||||
_updatePinnedMessages: function() {
|
||||
|
|
|
@ -149,6 +149,13 @@ module.exports = React.createClass({
|
|||
dis.dispatch({ action: 'show_right_panel' });
|
||||
},
|
||||
|
||||
onShareRoomClick: function(ev) {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
target: this.props.room,
|
||||
});
|
||||
},
|
||||
|
||||
_hasUnreadPins: function() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
|
@ -379,6 +386,14 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let shareRoomButton;
|
||||
if (this.props.inRoom) {
|
||||
shareRoomButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShareRoomClick} title={_t('Share room')}>
|
||||
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let rightPanelButtons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanelButtons =
|
||||
|
@ -400,6 +415,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settingsButton }
|
||||
{ pinnedEventsButton }
|
||||
{ shareRoomButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
'use strict';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -583,14 +585,18 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_makeGroupInviteTiles() {
|
||||
_makeGroupInviteTiles(filter) {
|
||||
const ret = [];
|
||||
const lcFilter = filter && filter.toLowerCase();
|
||||
|
||||
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
|
||||
for (const group of MatrixClientPeg.get().getGroups()) {
|
||||
if (group.myMembership !== 'invite') continue;
|
||||
|
||||
ret.push(<GroupInviteTile key={group.groupId} group={group} />);
|
||||
const {groupId, name, myMembership} = group;
|
||||
// filter to only groups in invite state and group_id starts with filter or group name includes it
|
||||
if (myMembership !== 'invite') continue;
|
||||
if (lcFilter && !groupId.toLowerCase().startsWith(lcFilter) &&
|
||||
!(name && name.toLowerCase().includes(lcFilter))) continue;
|
||||
ret.push(<GroupInviteTile key={groupId} group={group} collapsed={this.props.collapsed} />);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -604,13 +610,17 @@ module.exports = React.createClass({
|
|||
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
// XXX: we can't detect device-level (localStorage) settings onChange as the SettingsStore does not notify
|
||||
// so checking on every render is the sanest thing at this time.
|
||||
const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty');
|
||||
|
||||
const self = this;
|
||||
return (
|
||||
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
|
||||
autoshow={true} onScroll={self._whenScrolling} onResize={self._whenScrolling} wrappedRef={this._collectGemini}>
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={[]}
|
||||
extraTiles={this._makeGroupInviteTiles()}
|
||||
extraTiles={this._makeGroupInviteTiles(self.props.searchFilter)}
|
||||
label={_t('Community Invites')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
|
@ -619,6 +629,7 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||
|
@ -631,6 +642,7 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['m.favourite']}
|
||||
|
@ -643,7 +655,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
|
||||
label={_t('People')}
|
||||
|
@ -657,7 +670,8 @@ module.exports = React.createClass({
|
|||
alwaysShowHeader={true}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
|
||||
label={_t('Rooms')}
|
||||
|
@ -669,7 +683,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
{ Object.keys(self.state.lists).map((tagName) => {
|
||||
if (!tagName.match(STANDARD_TAGS_REGEX)) {
|
||||
|
@ -684,7 +699,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />;
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />;
|
||||
}
|
||||
}) }
|
||||
|
||||
|
@ -698,9 +714,17 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
|
||||
emptyContent={self.props.collapsed ? null :
|
||||
<div className="mx_RoomList_emptySubListTip_container">
|
||||
<div className="mx_RoomList_emptySubListTip">
|
||||
{ _t('You have no historical rooms') }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
label={_t('Historical')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
|
@ -708,10 +732,11 @@ module.exports = React.createClass({
|
|||
alwaysShowHeader={true}
|
||||
startAsHidden={true}
|
||||
showSpinner={self.state.isLoadingLeftRooms}
|
||||
onHeaderClick= {self.onArchivedHeaderClick}
|
||||
onHeaderClick={self.onArchivedHeaderClick}
|
||||
incomingCall={self.state.incomingCall}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
|
|
|
@ -395,7 +395,17 @@ module.exports = React.createClass({
|
|||
powerLevels["events"] = Object.assign({}, this.state.powerLevels["events"] || {});
|
||||
powerLevels["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
|
||||
} else {
|
||||
powerLevels[powerLevelKey] = value;
|
||||
const keyPath = powerLevelKey.split('.');
|
||||
let parentObj;
|
||||
let currentObj = powerLevels;
|
||||
for (const key of keyPath) {
|
||||
if (!currentObj[key]) {
|
||||
currentObj[key] = {};
|
||||
}
|
||||
parentObj = currentObj;
|
||||
currentObj = currentObj[key];
|
||||
}
|
||||
parentObj[keyPath[keyPath.length - 1]] = value;
|
||||
}
|
||||
this.setState({
|
||||
powerLevels,
|
||||
|
@ -664,6 +674,10 @@ module.exports = React.createClass({
|
|||
desc: _t('To remove other users\' messages, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
"notifications.room": {
|
||||
desc: _t('To notify everyone in the room, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
};
|
||||
|
||||
const banLevel = parseIntWithDefault(powerLevels.ban, powerLevelDescriptors.ban.defaultValue);
|
||||
|
@ -695,26 +709,57 @@ module.exports = React.createClass({
|
|||
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')}
|
||||
/>;
|
||||
|
||||
let userLevelsSection;
|
||||
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }.</div>; // default
|
||||
let mutedUsersSection;
|
||||
if (Object.keys(userLevels).length) {
|
||||
userLevelsSection =
|
||||
<div>
|
||||
<h3>{ _t('Privileged Users') }</h3>
|
||||
<ul className="mx_RoomSettings_userLevels">
|
||||
{ Object.keys(userLevels).map(function(user, i) {
|
||||
return (
|
||||
<li className="mx_RoomSettings_userLevel" key={user}>
|
||||
{ _t("%(user)s is a %(userRole)s", {
|
||||
user: user,
|
||||
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
|
||||
}) }
|
||||
</li>
|
||||
);
|
||||
const privilegedUsers = [];
|
||||
const mutedUsers = [];
|
||||
|
||||
Object.keys(userLevels).forEach(function(user) {
|
||||
if (userLevels[user] > defaultUserLevel) { // privileged
|
||||
privilegedUsers.push(<li className="mx_RoomSettings_userLevel" key={user}>
|
||||
{ _t("%(user)s is a %(userRole)s", {
|
||||
user: user,
|
||||
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
|
||||
}) }
|
||||
</ul>
|
||||
</div>;
|
||||
} else {
|
||||
userLevelsSection = <div>{ _t('No users have specific privileges in this room') }.</div>;
|
||||
</li>);
|
||||
} else if (userLevels[user] < defaultUserLevel) { // muted
|
||||
mutedUsers.push(<li className="mx_RoomSettings_userLevel" key={user}>
|
||||
{ _t("%(user)s is a %(userRole)s", {
|
||||
user: user,
|
||||
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
|
||||
}) }
|
||||
</li>);
|
||||
}
|
||||
});
|
||||
|
||||
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
|
||||
const comparator = (a, b) => {
|
||||
const plDiff = userLevels[b.key] - userLevels[a.key];
|
||||
return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase());
|
||||
};
|
||||
|
||||
privilegedUsers.sort(comparator);
|
||||
mutedUsers.sort(comparator);
|
||||
|
||||
if (privilegedUsers.length) {
|
||||
privilegedUsersSection =
|
||||
<div>
|
||||
<h3>{ _t('Privileged Users') }</h3>
|
||||
<ul className="mx_RoomSettings_userLevels">
|
||||
{ privilegedUsers }
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
if (mutedUsers.length) {
|
||||
mutedUsersSection =
|
||||
<div>
|
||||
<h3>{ _t('Muted Users') }</h3>
|
||||
<ul className="mx_RoomSettings_userLevels">
|
||||
{ mutedUsers }
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const banned = this.props.room.getMembersWithMembership("ban");
|
||||
|
@ -834,7 +879,16 @@ module.exports = React.createClass({
|
|||
const powerSelectors = Object.keys(powerLevelDescriptors).map((key, index) => {
|
||||
const descriptor = powerLevelDescriptors[key];
|
||||
|
||||
const value = parseIntWithDefault(powerLevels[key], descriptor.defaultValue);
|
||||
const keyPath = key.split('.');
|
||||
let currentObj = powerLevels;
|
||||
for (const prop of keyPath) {
|
||||
if (currentObj === undefined) {
|
||||
break;
|
||||
}
|
||||
currentObj = currentObj[prop];
|
||||
}
|
||||
|
||||
const value = parseIntWithDefault(currentObj, descriptor.defaultValue);
|
||||
return <div key={index} className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">
|
||||
{ descriptor.desc }
|
||||
|
@ -979,8 +1033,8 @@ module.exports = React.createClass({
|
|||
{ unfederatableSection }
|
||||
</div>
|
||||
|
||||
{ userLevelsSection }
|
||||
|
||||
{ privilegedUsersSection }
|
||||
{ mutedUsersSection }
|
||||
{ bannedUsersSection }
|
||||
|
||||
<h3>{ _t('Advanced') }</h3>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,19 +16,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
const classNames = require('classnames');
|
||||
import classNames from 'classnames';
|
||||
import dis from '../../../dispatcher';
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
const sdk = require('../../../index');
|
||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||
const RoomNotifs = require('../../../RoomNotifs');
|
||||
const FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import sdk from '../../../index';
|
||||
import {createMenu} from '../../structures/ContextualMenu';
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -72,16 +71,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_shouldShowMentionBadge: function() {
|
||||
return this.state.notifState != RoomNotifs.MUTE;
|
||||
return this.state.notifState !== RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
_isDirectMessageRoom: function(roomId) {
|
||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (dmRooms) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return Boolean(dmRooms);
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room) {
|
||||
|
@ -99,7 +94,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAccountData: function(accountDataEvent) {
|
||||
if (accountDataEvent.getType() == 'm.push_rules') {
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
this.setState({
|
||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||
});
|
||||
|
@ -187,6 +182,32 @@ module.exports = React.createClass({
|
|||
this.badgeOnMouseLeave();
|
||||
},
|
||||
|
||||
_showContextMenu: function(x, y, chevronOffset) {
|
||||
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
|
||||
createMenu(RoomTileContextMenu, {
|
||||
chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
this.props.refreshSubList();
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.preventDefault();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
badgeOnMouseEnter: function() {
|
||||
// Only allow non-guests to access the context menu
|
||||
// and only change it if it needs to change
|
||||
|
@ -200,37 +221,25 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onBadgeClicked: function(e) {
|
||||
// Only allow none guests to access the context menu
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
const self = this;
|
||||
ContextualMenu.createMenu(RoomTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: function() {
|
||||
self.setState({ menuDisplayed: false });
|
||||
self.props.refreshSubList();
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
}
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
this._showContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -250,7 +259,7 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_unread': this.props.unread,
|
||||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||
'mx_RoomTile_invited': (me && me.membership === 'invite'),
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
|
@ -268,7 +277,6 @@ module.exports = React.createClass({
|
|||
let name = this.state.roomName;
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
let badge;
|
||||
let badgeContent;
|
||||
|
||||
if (this.state.badgeHover || this.state.menuDisplayed) {
|
||||
|
@ -280,7 +288,7 @@ module.exports = React.createClass({
|
|||
badgeContent = '\u200B';
|
||||
}
|
||||
|
||||
badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let label;
|
||||
|
@ -301,7 +309,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
} else if (this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} dir="auto" />;
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
}
|
||||
|
||||
//var incomingCallBox;
|
||||
|
@ -312,16 +320,22 @@ module.exports = React.createClass({
|
|||
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
let directMessageIndicator;
|
||||
let dmIndicator;
|
||||
if (this._isDirectMessageRoom(this.props.room.roomId)) {
|
||||
directMessageIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />;
|
||||
dmIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />;
|
||||
}
|
||||
|
||||
return <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
return <AccessibleButton tabIndex="0"
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ directMessageIndicator }
|
||||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
|
|
|
@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var dis = require('../../../dispatcher');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
@ -77,25 +76,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderTooltip: function() {
|
||||
var label = this.props.room ? this.props.room.name : this.props.label;
|
||||
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
// NOTE: The additional 6 pixels for the left position, is to take account of the
|
||||
// tooltips chevron
|
||||
var parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
var style = {};
|
||||
const parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
let style = {};
|
||||
style = this._updatePosition(style);
|
||||
style.display = "block";
|
||||
|
||||
const tooltipClasses = classNames(
|
||||
"mx_RoomTooltip", this.props.tooltipClassName,
|
||||
);
|
||||
const tooltipClasses = classNames("mx_RoomTooltip", this.props.tooltipClassName);
|
||||
|
||||
var tooltip = (
|
||||
<div className={tooltipClasses} style={style} >
|
||||
<div className="mx_RoomTooltip_chevron"></div>
|
||||
{ label }
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div className="mx_RoomTooltip_chevron" />
|
||||
{ this.props.label }
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Widgets from '../../../utils/widgets';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -24,9 +23,14 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
|
||||
// We sit in a context menu, so the persisted element container needs to float
|
||||
// above it, so it needs a greater z-index than the ContextMenu
|
||||
const STICKERPICKER_Z_INDEX = 5000;
|
||||
|
||||
export default class Stickerpicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -67,7 +71,7 @@ export default class Stickerpicker extends React.Component {
|
|||
}
|
||||
|
||||
this.setState({showStickers: false});
|
||||
Widgets.removeStickerpickerWidgets().then(() => {
|
||||
WidgetUtils.removeStickerpickerWidgets().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
console.error('Failed to remove sticker picker widget', e);
|
||||
|
@ -119,7 +123,7 @@ export default class Stickerpicker extends React.Component {
|
|||
}
|
||||
|
||||
_updateWidget() {
|
||||
const stickerpickerWidget = Widgets.getStickerpickerWidgets()[0];
|
||||
const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
|
||||
this.setState({
|
||||
stickerpickerWidget,
|
||||
widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,
|
||||
|
@ -211,7 +215,7 @@ export default class Stickerpicker extends React.Component {
|
|||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement>
|
||||
<PersistedElement containerId="mx_persisted_stickerPicker" style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<AppTile
|
||||
collectWidgetMessaging={this._collectWidgetMessaging}
|
||||
id={stickerpickerWidget.id}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
|
@ -14,36 +15,28 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ChangeDisplayName',
|
||||
|
||||
_getDisplayName: function() {
|
||||
_getDisplayName: async function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.getProfileInfo(cli.credentials.userId).then(function(result) {
|
||||
let displayname = result.displayname;
|
||||
if (!displayname) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
displayname = "Guest " + MatrixClientPeg.get().getUserIdLocalpart();
|
||||
} else {
|
||||
displayname = MatrixClientPeg.get().getUserIdLocalpart();
|
||||
}
|
||||
}
|
||||
return displayname;
|
||||
}, function(error) {
|
||||
try {
|
||||
const res = await cli.getProfileInfo(cli.getUserId());
|
||||
return res.displayname;
|
||||
} catch (e) {
|
||||
throw new Error("Failed to fetch display name");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_changeDisplayName: function(new_displayname) {
|
||||
_changeDisplayName: function(newDisplayname) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.setDisplayName(new_displayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name");
|
||||
return cli.setDisplayName(newDisplayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name", e);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
|
@ -14,14 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const Modal = require("../../../Modal");
|
||||
const sdk = require("../../../index");
|
||||
|
||||
import dis from "../../../dispatcher";
|
||||
import Promise from 'bluebird';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -143,6 +143,9 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
cli.setPassword(authDict, newPassword).then(() => {
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({action: 'password_changed'});
|
||||
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this._optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue