Merge remote-tracking branch 'origin/develop' into matthew/e2e_backups
This commit is contained in:
commit
8fd7c4a66b
231 changed files with 13885 additions and 11701 deletions
|
@ -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 = {
|
||||
|
@ -52,11 +54,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
for (const roomId of dmRooms) {
|
||||
const room = client.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(client.credentials.userId);
|
||||
const highlight = (
|
||||
room.getUnreadNotificationCount('highlight') > 0 ||
|
||||
me.membership == "invite"
|
||||
);
|
||||
const isInvite = room.getMyMembership() === "invite";
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || isInvite;
|
||||
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={isInvite}
|
||||
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,
|
||||
|
|
|
@ -56,7 +56,7 @@ export default React.createClass({
|
|||
_checkGroupId: function(e) {
|
||||
let error = null;
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot not be empty.");
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export default React.createClass({
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
componentWillMount: function() {
|
||||
const config = SdkConfig.get();
|
||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||
this.defaultNoFederate = config.default_federate === false;
|
||||
|
@ -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 />
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ class GenericEditor extends DevtoolsComponent {
|
|||
<label htmlFor={id}>{ label }</label>
|
||||
</div>
|
||||
<div className="mx_DevTools_inputCell">
|
||||
<input id={id} className="mx_TextInputDialog_input" onChange={this._onChange} value={this.state[id]} size="32" />
|
||||
<input id={id} className="mx_TextInputDialog_input" onChange={this._onChange} value={this.state[id]} size="32" autoFocus={true} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -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,68 @@ 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"
|
||||
autoFocus={true}
|
||||
onChange={this.onQuery}
|
||||
value={this.props.query}
|
||||
placeholder={_t('Filter results')}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
|
||||
{ this.filterChildren() }
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
key={this.props.children[0] ? this.props.children[0].key : ''} />
|
||||
<TruncatedList getChildren={this.getChildren}
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this.createOverflowElement} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -377,10 +417,10 @@ class RoomStateExplorer extends DevtoolsComponent {
|
|||
const stateKeys = Object.keys(stateGroup);
|
||||
|
||||
let onClickFn;
|
||||
if (stateKeys.length > 1) {
|
||||
onClickFn = this.browseEventType(evType);
|
||||
} else if (stateKeys.length === 1) {
|
||||
if (stateKeys.length === 1 && stateKeys[0] === '') {
|
||||
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
|
||||
} else {
|
||||
onClickFn = this.browseEventType(evType);
|
||||
}
|
||||
|
||||
return <button className={classes} key={evType} onClick={onClickFn}>
|
||||
|
@ -485,7 +525,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,9 +67,10 @@ export default React.createClass({
|
|||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
cancelButton={this.props.cancelButton}
|
||||
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>;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue