Merge branch 'develop' into t3chguy/warn_self_mute

This commit is contained in:
Michael Telatynski 2018-06-29 19:17:43 +01:00 committed by GitHub
commit ea6e1b6f72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
157 changed files with 5426 additions and 3634 deletions

View file

@ -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>;
}
}

View file

@ -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>
);

View file

@ -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}

View file

@ -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 }

View file

@ -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,

View file

@ -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 />

View file

@ -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}
>

View file

@ -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")}

View file

@ -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) {

View 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>&nbsp;</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>;
}
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import { _t } from '../../../languageHandler';
import WidgetUtils from "../../../WidgetUtils";
import WidgetUtils from "../../../utils/WidgetUtils";
export default class AppPermission extends React.Component {
constructor(props) {

View file

@ -31,7 +31,7 @@ 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:'];
@ -319,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);

View file

@ -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}

View file

@ -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;
},
});

View file

@ -36,7 +36,7 @@ function getOrCreateContainer() {
}
// Greater than that of the ContextualMenu
const PE_Z_INDEX = 3000;
const PE_Z_INDEX = 5000;
/*
* Class of component that renders its children in a separate ReactDOM virtual tree

View file

@ -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
},
@ -81,12 +84,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;
@ -109,9 +114,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: {
@ -140,8 +147,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() {
@ -179,6 +199,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');
@ -229,6 +250,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, {

View file

@ -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} />;

View file

@ -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}

View file

@ -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() {

View file

@ -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>
);
},

View file

@ -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">

View file

@ -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...") :

View file

@ -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}>

View file

@ -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,

View file

@ -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 }),
}, "*");

View file

@ -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,25 +16,19 @@ 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,
@ -43,23 +38,21 @@ export default class extends React.Component {
/* 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 = {
@ -68,6 +61,9 @@ export default class extends React.Component {
decryptedBlob: null,
error: null,
imgError: false,
imgLoaded: false,
loadedImageDimensions: null,
hover: false,
};
}
@ -91,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();
@ -122,6 +118,8 @@ export default class extends React.Component {
}
onImageEnter(e) {
this.setState({ hover: true });
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return;
}
@ -130,6 +128,8 @@ export default class extends React.Component {
}
onImageLeave(e) {
this.setState({ hover: false });
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return;
}
@ -145,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() {
@ -164,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
@ -179,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) {
@ -210,7 +217,6 @@ export default class extends React.Component {
});
}).done();
}
this.fixupHeight();
this._afterComponentDidMount();
}
@ -221,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();
@ -238,60 +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 = this.props.maxImageHeight || 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() {
@ -306,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")) {
@ -333,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>;
}
}

View file

@ -18,143 +18,39 @@ limitations under the License.
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;
}
}

View file

@ -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>

View file

@ -62,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

View file

@ -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 || [] : [],
});
},

View file

@ -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

View file

@ -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>

View file

@ -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);
});
},

View file

@ -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');
@ -442,26 +443,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 +492,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 +610,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 +718,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 +745,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")}

View file

@ -652,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;
@ -725,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 }
@ -922,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>

View file

@ -159,54 +159,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);
}
onInputContentChanged(content: string, selection: {start: number, end: number}) {

View file

@ -28,7 +28,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';
@ -45,8 +45,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, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
@ -158,6 +157,7 @@ export default class MessageComposerInput extends React.Component {
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
this.onTextPasted = this.onTextPasted.bind(this);
this.focusComposer = this.focusComposer.bind(this);
const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
@ -271,13 +271,12 @@ export default class MessageComposerInput extends React.Component {
}
onAction = (payload) => {
const editor = this.refs.editor;
let contentState = this.state.editorState.getCurrentContent();
switch (payload.action) {
case 'reply_to_event':
case 'focus_composer':
editor.focus();
this.focusComposer();
break;
case 'insert_mention': {
// Pretend that we've autocompleted this user because keeping two code
@ -320,7 +319,7 @@ export default class MessageComposerInput extends React.Component {
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
editorState = EditorState.moveSelectionToEnd(editorState);
this.onEditorContentChanged(editorState);
editor.focus();
this.focusComposer();
}
}
break;
@ -722,7 +721,7 @@ export default class MessageComposerInput extends React.Component {
// Some commands (/join) require pills to be replaced with their text content
const commandText = this.removeMDLinks(contentState, ['#']);
const cmd = SlashCommands.processInput(this.props.room.roomId, commandText);
const cmd = processCommandInput(this.props.room.roomId, commandText);
if (cmd) {
if (!cmd.error) {
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown');
@ -1156,6 +1155,10 @@ export default class MessageComposerInput extends React.Component {
this.handleKeyCommand('toggle-mode');
};
focusComposer() {
this.refs.editor.focus();
}
render() {
const activeEditorState = this.state.originalEditorState || this.state.editorState;
@ -1180,9 +1183,9 @@ export default class MessageComposerInput extends React.Component {
activeEditorState.getCurrentContent().getBlocksAsArray());
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}

View file

@ -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 }

View file

@ -583,14 +583,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;
@ -607,10 +611,10 @@ module.exports = React.createClass({
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"
@ -701,6 +705,13 @@ module.exports = React.createClass({
onShowMoreRooms={self.onShowMoreRooms} />
<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,7 +719,7 @@ 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} />

View file

@ -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">

View file

@ -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>
);

View file

@ -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,6 +23,7 @@ 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';
@ -67,7 +67,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 +119,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,

View file

@ -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);
});
},

View file

@ -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({