Merge remote-tracking branch 'upstream/develop' into hs/upload-limits
This commit is contained in:
commit
5a72a5863f
232 changed files with 13386 additions and 11189 deletions
|
@ -87,7 +87,7 @@ module.exports = React.createClass({
|
|||
if (this.unmounted) return;
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
|
|
|
@ -26,7 +26,8 @@ module.exports = React.createClass({
|
|||
displayName: 'MemberAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: PropTypes.object.isRequired,
|
||||
member: PropTypes.object,
|
||||
fallbackUserId: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
|
@ -55,23 +56,30 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_getState: function(props) {
|
||||
if (!props.member) {
|
||||
console.error("MemberAvatar called somehow with null member");
|
||||
if (props.member) {
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod),
|
||||
};
|
||||
} else if (props.fallbackUserId) {
|
||||
return {
|
||||
name: props.fallbackUserId,
|
||||
title: props.fallbackUserId,
|
||||
};
|
||||
} else {
|
||||
console.error("MemberAvatar called somehow with null member or fallbackUserId");
|
||||
}
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod),
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
let {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
let userId = member ? member.userId : fallbackUserId;
|
||||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
|
@ -84,7 +92,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ import {ContentRepo} from "matrix-js-sdk";
|
|||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from "../../../index";
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
@ -107,58 +108,29 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
if (!props.room) return null;
|
||||
|
||||
const mlist = props.room.currentState.members;
|
||||
const userIds = [];
|
||||
const leftUserIds = [];
|
||||
// for .. in optimisation to return early if there are >2 keys
|
||||
for (const uid in mlist) {
|
||||
if (mlist.hasOwnProperty(uid)) {
|
||||
if (["join", "invite"].includes(mlist[uid].membership)) {
|
||||
userIds.push(uid);
|
||||
} else {
|
||||
leftUserIds.push(uid);
|
||||
}
|
||||
}
|
||||
if (userIds.length > 2) {
|
||||
return null;
|
||||
}
|
||||
const room = props.room;
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (userIds.length == 2) {
|
||||
let theOtherGuy = null;
|
||||
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = mlist[userIds[1]];
|
||||
} else {
|
||||
theOtherGuy = mlist[userIds[0]];
|
||||
}
|
||||
return theOtherGuy.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
} else if (userIds.length == 1) {
|
||||
// The other 1-1 user left, leaving just the current user, so show the left user's avatar
|
||||
if (leftUserIds.length === 1) {
|
||||
return mlist[leftUserIds[0]].getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return mlist[userIds[0]].getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (otherUserId) {
|
||||
otherMember = room.getMember(otherUserId);
|
||||
} else {
|
||||
return null;
|
||||
// if the room is not marked as a 1:1, but only has max 2 members
|
||||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
if (otherMember) {
|
||||
return otherMember.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
onRoomAvatarClick: function() {
|
||||
|
|
|
@ -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';
|
||||
|
@ -179,7 +178,7 @@ module.exports = React.createClass({
|
|||
onQuoteClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'quote',
|
||||
text: this.props.eventTileOps.getInnerText(),
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
@ -220,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') }
|
||||
|
@ -228,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') }
|
||||
|
@ -236,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') }
|
||||
|
@ -244,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 = (
|
||||
|
|
|
@ -346,20 +346,18 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const myMember = this.props.room.getMember(
|
||||
MatrixClientPeg.get().credentials.userId,
|
||||
);
|
||||
const myMembership = this.props.room.getMyMembership();
|
||||
|
||||
// Can't set notif level or tags on non-join rooms
|
||||
if (myMember.membership !== 'join') {
|
||||
return this._renderLeaveMenu(myMember.membership);
|
||||
if (myMembership !== 'join') {
|
||||
return this._renderLeaveMenu(myMembership);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this._renderNotifMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderLeaveMenu(myMember.membership) }
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderRoomTagMenu() }
|
||||
</div>
|
||||
|
|
|
@ -140,9 +140,9 @@ export default class BugReportDialog extends React.Component {
|
|||
"not contain messages.",
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
<p><b>
|
||||
{ _t(
|
||||
"Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.",
|
||||
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.",
|
||||
{},
|
||||
{
|
||||
a: (sub) => <a
|
||||
|
@ -153,13 +153,13 @@ export default class BugReportDialog extends React.Component {
|
|||
</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
</b></p>
|
||||
<div className="mx_BugReportDialog_field_container">
|
||||
<label
|
||||
htmlFor="mx_BugReportDialog_issueUrl"
|
||||
className="mx_BugReportDialog_field_label"
|
||||
>
|
||||
{ _t("GitHub issue link:") }
|
||||
{ _t("What GitHub issue are these logs for?") }
|
||||
</label>
|
||||
<input
|
||||
id="mx_BugReportDialog_issueUrl"
|
||||
|
@ -167,7 +167,7 @@ export default class BugReportDialog extends React.Component {
|
|||
className="mx_BugReportDialog_field_input"
|
||||
onChange={this._onIssueUrlChange}
|
||||
value={this.state.issueUrl}
|
||||
placeholder="https://github.com/vector-im/riot-web/issues/1337"
|
||||
placeholder="https://github.com/vector-im/riot-web/issues/..."
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_BugReportDialog_field_container">
|
||||
|
|
|
@ -54,8 +54,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
for (const roomId of dmRooms) {
|
||||
const room = client.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(client.credentials.userId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === "invite";
|
||||
const isInvite = room.getMyMembership() === "invite";
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || isInvite;
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
transparent={true}
|
||||
|
@ -63,7 +63,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership === "invite"}
|
||||
isInvite={isInvite}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
|
|
|
@ -56,7 +56,7 @@ export default React.createClass({
|
|||
_checkGroupId: function(e) {
|
||||
let error = null;
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot not be empty.");
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export default React.createClass({
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
componentWillMount: function() {
|
||||
const config = SdkConfig.get();
|
||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||
this.defaultNoFederate = config.default_federate === false;
|
||||
|
|
|
@ -61,7 +61,7 @@ class GenericEditor extends DevtoolsComponent {
|
|||
<label htmlFor={id}>{ label }</label>
|
||||
</div>
|
||||
<div className="mx_DevTools_inputCell">
|
||||
<input id={id} className="mx_TextInputDialog_input" onChange={this._onChange} value={this.state[id]} size="32" />
|
||||
<input id={id} className="mx_TextInputDialog_input" onChange={this._onChange} value={this.state[id]} size="32" autoFocus={true} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -242,6 +242,9 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
}
|
||||
|
||||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
|
@ -249,31 +252,68 @@ class FilteredList extends React.Component {
|
|||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static filterChildren(children, query) {
|
||||
if (!query) return children;
|
||||
const lcQuery = query.toLowerCase();
|
||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onQuery = this.onQuery.bind(this);
|
||||
|
||||
this.state = {
|
||||
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
};
|
||||
}
|
||||
|
||||
onQuery(ev) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||
this.setState({
|
||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
});
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
this.setState({
|
||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||
});
|
||||
};
|
||||
|
||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||
</button>;
|
||||
};
|
||||
|
||||
onQuery = (ev) => {
|
||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
filterChildren() {
|
||||
if (this.props.query) {
|
||||
const lowerQuery = this.props.query.toLowerCase();
|
||||
return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
getChildren = (start: number, end: number) => {
|
||||
return this.state.filteredChildren.slice(start, end);
|
||||
};
|
||||
|
||||
getChildCount = (): number => {
|
||||
return this.state.filteredChildren.length;
|
||||
};
|
||||
|
||||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<input size="64"
|
||||
autoFocus={true}
|
||||
onChange={this.onQuery}
|
||||
value={this.props.query}
|
||||
placeholder={_t('Filter results')}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
|
||||
{ this.filterChildren() }
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
key={this.props.children[0] ? this.props.children[0].key : ''} />
|
||||
<TruncatedList getChildren={this.getChildren}
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this.createOverflowElement} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -377,10 +417,10 @@ class RoomStateExplorer extends DevtoolsComponent {
|
|||
const stateKeys = Object.keys(stateGroup);
|
||||
|
||||
let onClickFn;
|
||||
if (stateKeys.length > 1) {
|
||||
onClickFn = this.browseEventType(evType);
|
||||
} else if (stateKeys.length === 1) {
|
||||
if (stateKeys.length === 1 && stateKeys[0] === '') {
|
||||
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
|
||||
} else {
|
||||
onClickFn = this.browseEventType(evType);
|
||||
}
|
||||
|
||||
return <button className={classes} key={evType} onClick={onClickFn}>
|
||||
|
|
39
src/components/views/dialogs/LazyLoadingDisabledDialog.js
Normal file
39
src/components/views/dialogs/LazyLoadingDisabledDialog.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default (props) => {
|
||||
const description1 =
|
||||
_t("You've previously used Riot on %(host)s with lazy loading of members enabled. " +
|
||||
"In this version lazy loading is disabled. " +
|
||||
"As the local cache is not compatible between these two settings, " +
|
||||
"Riot needs to resync your account.",
|
||||
{host: props.host});
|
||||
const description2 = _t("If the other version of Riot is still open in another tab, " +
|
||||
"please close it as using Riot on the same host with both " +
|
||||
"lazy loading enabled and disabled simultaneously will cause issues.");
|
||||
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
title={_t("Incompatible local cache")}
|
||||
description={<div><p>{description1}</p><p>{description2}</p></div>}
|
||||
button={_t("Clear cache and resync")}
|
||||
onFinished={props.onFinished}
|
||||
/>);
|
||||
};
|
33
src/components/views/dialogs/LazyLoadingResyncDialog.js
Normal file
33
src/components/views/dialogs/LazyLoadingResyncDialog.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default (props) => {
|
||||
const description =
|
||||
_t("Riot now uses 3-5x less memory, by only loading information about other users"
|
||||
+ " when needed. Please wait whilst we resynchronise with the server!");
|
||||
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
title={_t("Updating Riot")}
|
||||
description={<div>{description}</div>}
|
||||
button={_t("OK")}
|
||||
onFinished={props.onFinished}
|
||||
/>);
|
||||
};
|
|
@ -62,6 +62,7 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description }
|
||||
|
|
106
src/components/views/dialogs/RoomUpgradeDialog.js
Normal file
106
src/components/views/dialogs/RoomUpgradeDialog.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
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 sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'RoomUpgradeDialog',
|
||||
|
||||
propTypes: {
|
||||
room: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._targetVersion = this.props.room.shouldUpgradeToVersion();
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
};
|
||||
},
|
||||
|
||||
_onCancelClick: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_onUpgradeClick: function() {
|
||||
this.setState({busy: true});
|
||||
MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
|
||||
title: _t("Failed to upgrade room"),
|
||||
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({busy: false});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
|
||||
let buttons;
|
||||
if (this.state.busy) {
|
||||
buttons = <Spinner />;
|
||||
} else {
|
||||
buttons = <DialogButtons
|
||||
primaryButton={_t(
|
||||
'Upgrade this room to version %(version)s',
|
||||
{version: this._targetVersion},
|
||||
)}
|
||||
primaryButtonClass="danger"
|
||||
hasCancel={true}
|
||||
onPrimaryButtonClick={this._onUpgradeClick}
|
||||
focus={this.props.focus}
|
||||
onCancel={this._onCancelClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_RoomUpgradeDialog"
|
||||
onFinished={this.onCancelled}
|
||||
title={_t("Upgrade Room Version")}
|
||||
contentId='mx_Dialog_content'
|
||||
onFinished={this.props.onFinished}
|
||||
hasCancel={true}
|
||||
>
|
||||
<p>
|
||||
{_t(
|
||||
"Upgrading this room requires closing down the current " +
|
||||
"instance of the room and creating a new room it its place. " +
|
||||
"To give room members the best possible experience, we will:",
|
||||
)}
|
||||
</p>
|
||||
<ol>
|
||||
<li>{_t("Create a new room with the same name, description and avatar")}</li>
|
||||
<li>{_t("Update any local room aliases to point to the new room")}</li>
|
||||
<li>{_t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room")}</li>
|
||||
<li>{_t("Put a link back to the old room at the start of the new room so people can see old messages")}</li>
|
||||
</ol>
|
||||
{buttons}
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
@ -31,8 +32,9 @@ 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';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
@ -40,9 +42,13 @@ const ENABLE_REACT_PERF = false;
|
|||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = 'widget_' + this.props.id;
|
||||
|
||||
this.state = this._getNewState(props);
|
||||
|
||||
this._onWidgetAction = this._onWidgetAction.bind(this);
|
||||
this._onAction = this._onAction.bind(this);
|
||||
this._onMessage = this._onMessage.bind(this);
|
||||
this._onLoaded = this._onLoaded.bind(this);
|
||||
this._onEditClick = this._onEditClick.bind(this);
|
||||
|
@ -50,7 +56,6 @@ export default class AppTile extends React.Component {
|
|||
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||
this._onInitialLoad = this._onInitialLoad.bind(this);
|
||||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
||||
|
@ -66,9 +71,12 @@ export default class AppTile extends React.Component {
|
|||
_getNewState(newProps) {
|
||||
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
|
||||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
return {
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||
// True while the iframe content is loading
|
||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||
widgetUrl: this._addWurlParams(newProps.url),
|
||||
widgetPermissionId: widgetPermissionId,
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
|
@ -77,9 +85,6 @@ export default class AppTile extends React.Component {
|
|||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
allowedCapabilities: (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) ?
|
||||
this.props.whitelistCapabilities : [],
|
||||
requestedCapabilities: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -89,7 +94,7 @@ export default class AppTile extends React.Component {
|
|||
* @return {Boolean} True if capability supported
|
||||
*/
|
||||
_hasCapability(capability) {
|
||||
return this.state.allowedCapabilities.some((c) => {return c === capability;});
|
||||
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,8 +117,9 @@ export default class AppTile extends React.Component {
|
|||
const params = qs.parse(u.query);
|
||||
// Append widget ID to query parameters
|
||||
params.widgetId = this.props.id;
|
||||
// Append current / parent URL
|
||||
params.parentUrl = window.location.href;
|
||||
// Append current / parent URL, minus the hash because that will change when
|
||||
// we view a different room (ie. may change for persistent widgets)
|
||||
params.parentUrl = window.location.href.split('#', 2)[0];
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
|
||||
|
@ -142,30 +148,22 @@ export default class AppTile extends React.Component {
|
|||
window.addEventListener('message', this._onMessage, false);
|
||||
|
||||
// Widget action listeners
|
||||
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Allow parents to access widget messaging
|
||||
if (this.props.collectWidgetMessaging) {
|
||||
this.props.collectWidgetMessaging(this.widgetMessaging);
|
||||
}
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Widget action listeners
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
// Widget postMessage listeners
|
||||
try {
|
||||
if (this.widgetMessaging) {
|
||||
this.widgetMessaging.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
||||
}
|
||||
// Jitsi listener
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
|
||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget();
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -286,7 +284,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
_onSnapshotClick(e) {
|
||||
console.warn("Requesting widget snapshot");
|
||||
this.widgetMessaging.getScreenshot()
|
||||
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
|
@ -319,15 +317,18 @@ 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,
|
||||
).then(() => {
|
||||
return WidgetUtils.waitForRoomWidget(this.props.id, this.props.room.roomId, false);
|
||||
}).catch((e) => {
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Failed to remove widget', '', ErrorDialog, {
|
||||
title: _t('Failed to remove widget'),
|
||||
description: _t('An error ocurred whilst trying to remove the widget from the room'),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
|
@ -344,19 +345,20 @@ export default class AppTile extends React.Component {
|
|||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
if (!this.widgetMessaging) {
|
||||
this._onInitialLoad();
|
||||
if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) {
|
||||
this._setupWidgetMessaging();
|
||||
}
|
||||
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on initial load of the widget iframe
|
||||
*/
|
||||
_onInitialLoad() {
|
||||
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
|
||||
_setupWidgetMessaging() {
|
||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||
// in ActiveWidgetStore.
|
||||
const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
|
||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
||||
// Allow whitelisted capabilities
|
||||
|
@ -368,16 +370,15 @@ export default class AppTile extends React.Component {
|
|||
}, this.props.whitelistCapabilities);
|
||||
|
||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties:`,
|
||||
requestedWhitelistCapabilies);
|
||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
|
||||
requestedWhitelistCapabilies,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||
this.setState({
|
||||
requestedCapabilities,
|
||||
allowedCapabilities: this.state.allowedCapabilities.concat(requestedWhitelistCapabilies),
|
||||
});
|
||||
|
||||
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
|
||||
|
||||
if (this.props.onCapabilityRequest) {
|
||||
this.props.onCapabilityRequest(requestedCapabilities);
|
||||
|
@ -387,7 +388,7 @@ export default class AppTile extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onWidgetAction(payload) {
|
||||
_onAction(payload) {
|
||||
if (payload.widgetId === this.props.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
|
@ -435,6 +436,11 @@ export default class AppTile extends React.Component {
|
|||
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||
localStorage.removeItem(this.state.widgetPermissionId);
|
||||
this.setState({hasPermissionToLoad: false});
|
||||
|
||||
// Force the widget to be non-persistent
|
||||
ActiveWidgetStore.destroyPersistentWidget();
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
|
||||
formatAppTileName() {
|
||||
|
@ -527,6 +533,8 @@ export default class AppTile extends React.Component {
|
|||
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
|
||||
const iframeFeatures = "microphone; camera; encrypted-media;";
|
||||
|
||||
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
||||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className="mx_AppLoading_spinner_fadeIn">
|
||||
|
@ -535,20 +543,20 @@ export default class AppTile extends React.Component {
|
|||
);
|
||||
if (this.state.initialising) {
|
||||
appTileBody = (
|
||||
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ loadingElement }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.hasPermissionToLoad == true) {
|
||||
if (this.isMixedContent()) {
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={appTileBodyClass}>
|
||||
<AppWarning errorMsg="Error - Mixed content" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
appTileBody = (
|
||||
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
|
@ -565,11 +573,24 @@ export default class AppTile extends React.Component {
|
|||
></iframe>
|
||||
</div>
|
||||
);
|
||||
// if the widget would be allowed to remian on screen, we must put it in
|
||||
// a PersistedElement from the get-go, otherwise the iframe will be
|
||||
// re-mounted later when we do.
|
||||
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={appTileBodyClass}>
|
||||
<AppPermission
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
url={this.state.widgetUrl}
|
||||
|
@ -597,8 +618,17 @@ export default class AppTile extends React.Component {
|
|||
const reloadWidgetIcon = 'img/button-refresh.svg';
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
let appTileClass;
|
||||
if (this.props.miniMode) {
|
||||
appTileClass = 'mx_AppTile_mini';
|
||||
} else if (this.props.fullWidth) {
|
||||
appTileClass = 'mx_AppTileFullWidth';
|
||||
} else {
|
||||
appTileClass = 'mx_AppTile';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div className={appTileClass} id={this.props.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
|
@ -682,6 +712,8 @@ AppTile.propTypes = {
|
|||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: PropTypes.bool,
|
||||
// Optional. If set, renders a smaller view of the widget
|
||||
miniMode: PropTypes.bool,
|
||||
// UserId of the current user
|
||||
userId: PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
|
@ -734,4 +766,5 @@ AppTile.defaultProps = {
|
|||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
userWidget: false,
|
||||
miniMode: false,
|
||||
};
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const AppWarning = (props) => {
|
||||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||
<img src='img/warning.svg' alt='' />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -61,7 +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);
|
||||
|
|
|
@ -14,30 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
const ContainerId = "mx_PersistedElement";
|
||||
function getContainer(containerId) {
|
||||
return document.getElementById(containerId);
|
||||
}
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(ContainerId);
|
||||
function getOrCreateContainer(containerId) {
|
||||
let container = getContainer(containerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = ContainerId;
|
||||
container.id = containerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// Greater than that of the ContextualMenu
|
||||
const PE_Z_INDEX = 5000;
|
||||
|
||||
/*
|
||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||
* in a container element appended to document.body.
|
||||
|
@ -50,14 +54,57 @@ const PE_Z_INDEX = 5000;
|
|||
* bounding rect as the parent of PE.
|
||||
*/
|
||||
export default class PersistedElement extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
// Unique identifier for this PersistedElement instance
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
||||
this.collectChild = this.collectChild.bind(this);
|
||||
this._repositionChild = this._repositionChild.bind(this);
|
||||
this._onAction = this._onAction.bind(this);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this._repositionChild);
|
||||
// Annoyingly, a resize observer is insufficient, since we also care
|
||||
// about when the element moves on the screen without changing its
|
||||
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
||||
// for this, so we bodge it by listening for document resize and
|
||||
// the timeline_resize action.
|
||||
window.addEventListener('resize', this._repositionChild);
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the DOM elements created when a PersistedElement with the given
|
||||
* persistKey was mounted. The DOM elements will be re-added if another
|
||||
* PeristedElement is mounted in the future.
|
||||
*
|
||||
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
||||
*/
|
||||
static destroyElement(persistKey) {
|
||||
const container = getContainer('mx_persistedElement_' + persistKey);
|
||||
if (container) {
|
||||
container.remove();
|
||||
}
|
||||
}
|
||||
|
||||
static isMounted(persistKey) {
|
||||
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
||||
}
|
||||
|
||||
collectChildContainer(ref) {
|
||||
if (this.childContainer) {
|
||||
this.resizeObserver.unobserve(this.childContainer);
|
||||
}
|
||||
this.childContainer = ref;
|
||||
if (ref) {
|
||||
this.resizeObserver.observe(ref);
|
||||
}
|
||||
}
|
||||
|
||||
collectChild(ref) {
|
||||
|
@ -75,6 +122,19 @@ export default class PersistedElement extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.updateChildVisibility(this.child, false);
|
||||
this.resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', this._repositionChild);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.action === 'timeline_resize') {
|
||||
this._repositionChild();
|
||||
}
|
||||
}
|
||||
|
||||
_repositionChild() {
|
||||
this.updateChildPosition(this.child, this.childContainer);
|
||||
}
|
||||
|
||||
updateChild() {
|
||||
|
@ -97,18 +157,16 @@ export default class PersistedElement extends React.Component {
|
|||
left: parentRect.left + 'px',
|
||||
width: parentRect.width + 'px',
|
||||
height: parentRect.height + 'px',
|
||||
zIndex: PE_Z_INDEX,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = <div ref={this.collectChild}>
|
||||
const content = <div ref={this.collectChild} style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(content, getOrCreateContainer());
|
||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||
|
||||
return <div ref={this.collectChildContainer}></div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
96
src/components/views/elements/PersistentApp.js
Normal file
96
src/components/views/elements/PersistentApp.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
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 RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PersistentApp',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(payload) {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
_onActiveWidgetStoreUpdate: function() {
|
||||
this.setState({
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.persistentWidgetId) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
if (this.state.roomId !== persistentWidgetInRoomId) {
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||
});
|
||||
const app = WidgetUtils.makeAppConfig(
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId,
|
||||
);
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
|
||||
const AppTile = sdk.getComponent('elements.AppTile');
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={true}
|
||||
room={persistentWidgetInRoom}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
show={true}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
showDelete={false}
|
||||
showMinimise={false}
|
||||
miniMode={true}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
|
@ -62,6 +62,8 @@ const Pill = React.createClass({
|
|||
room: PropTypes.instanceOf(Room),
|
||||
// Whether to include an avatar in the pill
|
||||
shouldShowPillAvatar: PropTypes.bool,
|
||||
// Whether to render this pill as if it were highlit by a selection
|
||||
isSelected: PropTypes.bool,
|
||||
},
|
||||
|
||||
|
||||
|
@ -185,6 +187,9 @@ const Pill = React.createClass({
|
|||
getContent: () => {
|
||||
return {avatar_url: resp.avatar_url};
|
||||
},
|
||||
getDirectionalContent: function() {
|
||||
return this.getContent();
|
||||
},
|
||||
};
|
||||
this.setState({member});
|
||||
}).catch((err) => {
|
||||
|
@ -229,7 +234,7 @@ const Pill = React.createClass({
|
|||
if (member) {
|
||||
userId = member.userId;
|
||||
member.rawDisplayName = member.rawDisplayName || '';
|
||||
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
||||
linkText = member.rawDisplayName;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <MemberAvatar member={member} width={16} height={16} />;
|
||||
}
|
||||
|
@ -268,6 +273,7 @@ const Pill = React.createClass({
|
|||
|
||||
const classes = classNames(pillClass, {
|
||||
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
||||
"mx_UserPill_selected": this.props.isSelected,
|
||||
});
|
||||
|
||||
if (this.state.pillType) {
|
||||
|
|
|
@ -51,7 +51,7 @@ export default class CookieBar extends React.Component {
|
|||
const toolbarClasses = "mx_MatrixToolbar";
|
||||
return (
|
||||
<div className={toolbarClasses}>
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning" />
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ this.props.policyUrl ? _t(
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
|
@ -95,7 +95,7 @@ export default class CookieBar extends React.Component {
|
|||
{ _t("Yes, I want to help!") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
|
||||
<img src="img/cancel.svg" width="18" height="18" />
|
||||
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -35,11 +35,11 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning"/>
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
|
||||
</div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/></AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -96,7 +96,7 @@ export default React.createClass({
|
|||
}
|
||||
return (
|
||||
<div className="mx_MatrixToolbar">
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning"/>
|
||||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{_t("A new version of Riot is available.")}
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,7 @@ export default React.createClass({
|
|||
src="img/warning.svg"
|
||||
width="24"
|
||||
height="23"
|
||||
alt="Warning"
|
||||
alt=""
|
||||
/>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t(
|
||||
|
|
98
src/components/views/globals/ServerLimitBar.js
Normal file
98
src/components/views/globals/ServerLimitBar.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
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 classNames from 'classnames';
|
||||
import { _td } from '../../../languageHandler';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
export default React.createClass({
|
||||
propTypes: {
|
||||
// 'hard' if the logged in user has been locked out, 'soft' if they haven't
|
||||
kind: PropTypes.string,
|
||||
adminContact: PropTypes.string,
|
||||
// The type of limit that has been hit.
|
||||
limitType: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
kind: 'hard',
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const toolbarClasses = {
|
||||
'mx_MatrixToolbar': true,
|
||||
};
|
||||
|
||||
let adminContact;
|
||||
let limitError;
|
||||
if (this.props.kind === 'hard') {
|
||||
toolbarClasses['mx_MatrixToolbar_error'] = true;
|
||||
|
||||
adminContact = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to continue using the service."),
|
||||
},
|
||||
);
|
||||
limitError = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
|
||||
'': _td("This homeserver has exceeded one of its resource limits."),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
toolbarClasses['mx_MatrixToolbar_info'] = true;
|
||||
adminContact = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to get this limit increased."),
|
||||
},
|
||||
);
|
||||
limitError = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit so " +
|
||||
"<b>some users will not be able to log in</b>.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits so " +
|
||||
"<b>some users will not be able to log in</b>.",
|
||||
),
|
||||
},
|
||||
{'b': sub => <b>{sub}</b>},
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classNames(toolbarClasses)}>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{limitError}
|
||||
{' '}
|
||||
{adminContact}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -71,9 +71,9 @@ export default React.createClass({
|
|||
|
||||
let image;
|
||||
if (doneStatuses.includes(this.props.status)) {
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt={warning}/>;
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="" />;
|
||||
} else {
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/spinner.gif" width="24" height="23" alt={message}/>;
|
||||
image = <img className="mx_MatrixToolbar_warning" src="img/spinner.gif" width="24" height="23" alt="" />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -83,7 +83,7 @@ export default React.createClass({
|
|||
{message}
|
||||
</div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.hideToolbar}>
|
||||
<img src="img/cancel.svg" width="18" height="18" />
|
||||
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -134,7 +134,7 @@ export default React.createClass({
|
|||
</EmojiText>;
|
||||
|
||||
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||
const badgeClasses = classNames('mx_RoomSubList_badge mx_RoomSubList_badgeHighlight', {
|
||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_badgeButton': badgeEllipsis,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -49,7 +50,7 @@ module.exports = React.createClass({
|
|||
teamsConfig: PropTypes.shape({
|
||||
// Email address to request new teams
|
||||
supportEmail: PropTypes.string,
|
||||
teams: PropTypes.arrayOf(React.PropTypes.shape({
|
||||
teams: PropTypes.arrayOf(PropTypes.shape({
|
||||
// The displayed name of the team
|
||||
"name": PropTypes.string,
|
||||
// The domain of team email addresses
|
||||
|
@ -60,6 +61,7 @@ module.exports = React.createClass({
|
|||
minPasswordLength: PropTypes.number,
|
||||
onError: PropTypes.func,
|
||||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -180,12 +182,16 @@ module.exports = React.createClass({
|
|||
});
|
||||
}
|
||||
const emailValid = email === '' || Email.looksValid(email);
|
||||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) {
|
||||
this.markFieldValid(field_id, false, "RegistrationForm.ERR_MISSING_EMAIL");
|
||||
} else this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
case FIELD_PHONE_NUMBER:
|
||||
const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
|
||||
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
if (this._authStepIsRequired('m.login.msisdn') && (!phoneNumberValid || phoneNumber === '')) {
|
||||
this.markFieldValid(field_id, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER");
|
||||
} else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
// XXX: SPEC-1
|
||||
|
@ -273,12 +279,18 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_authStepIsRequired(step) {
|
||||
// A step is required if no flow exists which does not include that step
|
||||
// (Notwithstanding setups like either email or msisdn being required)
|
||||
return !this.props.flows.some((flow) => {
|
||||
return !flow.stages.includes(step);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const self = this;
|
||||
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
const emailPlaceholder = theme === 'status' ? _t("Email address") : _t("Email address (optional)");
|
||||
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ? _t("Email address") : _t("Email address (optional)");
|
||||
|
||||
const emailSection = (
|
||||
<div>
|
||||
|
@ -315,6 +327,7 @@ module.exports = React.createClass({
|
|||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
let phoneSection;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ? _t("Mobile phone number") : _t("Mobile phone number (optional)");
|
||||
phoneSection = (
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||
|
@ -324,7 +337,7 @@ module.exports = React.createClass({
|
|||
showPrefix={true}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
placeholder={phonePlaceholder}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER,
|
||||
|
|
|
@ -76,7 +76,7 @@ export default class MImageBody extends React.Component {
|
|||
onClientSync(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected && this.state.imgError) {
|
||||
// Load the image again
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MImageBody from './MImageBody';
|
||||
import sdk from '../../../index';
|
||||
|
||||
|
|
63
src/components/views/messages/RoomCreate.js
Normal file
63
src/components/views/messages/RoomCreate.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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 dis from '../../../dispatcher';
|
||||
import { makeEventPermalink } from '../../../matrix-to';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomCreate',
|
||||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
_onLinkClicked: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const predecessor = this.props.mxEvent.getContent()['predecessor'];
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: predecessor['event_id'],
|
||||
highlighted: true,
|
||||
room_id: predecessor['room_id'],
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const predecessor = this.props.mxEvent.getContent()['predecessor'];
|
||||
if (predecessor === undefined) {
|
||||
return <div />; // We should never have been instaniated in this case
|
||||
}
|
||||
return <div className="mx_CreateEvent">
|
||||
<img className="mx_CreateEvent_image" src="img/room-continuation.svg" />
|
||||
<div className="mx_CreateEvent_header">
|
||||
{_t("This room is a continuation of another conversation.")}
|
||||
</div>
|
||||
<a className="mx_CreateEvent_link"
|
||||
href={makeEventPermalink(predecessor['room_id'], predecessor['event_id'])}
|
||||
onClick={this._onLinkClicked}
|
||||
>
|
||||
{_t("Click here to see older messages.")}
|
||||
</a>
|
||||
</div>;
|
||||
},
|
||||
});
|
|
@ -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 || [] : [],
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -98,7 +96,7 @@ export default React.createClass({
|
|||
render() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = this.props;
|
||||
let name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
|
@ -111,9 +109,6 @@ export default React.createClass({
|
|||
this.state.userGroups, this.state.relatedGroups,
|
||||
);
|
||||
|
||||
// Backwards-compatible replacing of "(IRC)" with AS user flair
|
||||
name = displayedGroups.length > 0 ? name.replace(' (IRC)', '') : name;
|
||||
|
||||
flair = <Flair key='flair'
|
||||
userId={mxEvent.getSender()}
|
||||
groups={displayedGroups}
|
||||
|
|
|
@ -203,7 +203,7 @@ module.exports = React.createClass({
|
|||
// update the current node with one that's now taken its place
|
||||
node = pillContainer;
|
||||
}
|
||||
} else if (node.nodeType == Node.TEXT_NODE) {
|
||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
|
||||
let currentTextNode = node;
|
||||
|
@ -232,6 +232,12 @@ module.exports = React.createClass({
|
|||
if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) {
|
||||
// Now replace all those nodes with Pills
|
||||
for (const roomNotifTextNode of roomNotifTextNodes) {
|
||||
// Set the next node to be processed to the one after the node
|
||||
// we're adding now, since we've just inserted nodes into the structure
|
||||
// we're iterating over.
|
||||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
||||
node = roomNotifTextNode.nextSibling;
|
||||
|
||||
const pillContainer = document.createElement('span');
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pill = <Pill
|
||||
|
@ -243,12 +249,6 @@ module.exports = React.createClass({
|
|||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
||||
|
||||
// Set the next node to be processed to the one after the node
|
||||
// we're adding now, since we've just inserted nodes into the structure
|
||||
// we're iterating over.
|
||||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
||||
node = roomNotifTextNode.nextSibling;
|
||||
}
|
||||
// Nothing else to do for a text node (and we don't need to advance
|
||||
// the loop pointer because we did it above)
|
||||
|
@ -340,7 +340,18 @@ module.exports = React.createClass({
|
|||
}, 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);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 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.
|
||||
|
@ -97,18 +98,19 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// save new canonical alias
|
||||
let oldCanonicalAlias = null;
|
||||
if (this.props.canonicalAliasEvent) {
|
||||
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
|
||||
}
|
||||
if (oldCanonicalAlias !== this.state.canonicalAlias) {
|
||||
|
||||
let newCanonicalAlias = this.state.canonicalAlias;
|
||||
|
||||
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
|
||||
console.log("AliasSettings: Updating canonical alias");
|
||||
promises = [Promise.all(promises).then(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.roomId, "m.room.canonical_alias", {
|
||||
alias: this.state.canonicalAlias,
|
||||
alias: newCanonicalAlias,
|
||||
}, "",
|
||||
),
|
||||
)];
|
||||
|
@ -145,6 +147,7 @@ module.exports = React.createClass({
|
|||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
|
||||
this.state.domainToAliases[localDomain].push(alias);
|
||||
|
@ -161,11 +164,18 @@ module.exports = React.createClass({
|
|||
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.props.canonicalAlias) {
|
||||
this.setState({
|
||||
canonicalAlias: alias
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLocalAliasChanged: function(alias, index) {
|
||||
if (alias === "") return; // hit the delete button to delete please
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain][index] = alias;
|
||||
} else {
|
||||
|
@ -184,10 +194,15 @@ module.exports = React.createClass({
|
|||
// promptly setState anyway, it's just about acceptable. The alternative
|
||||
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||
// that, but why bother when we can cut this corner.
|
||||
this.state.domainToAliases[localDomain].splice(index, 1);
|
||||
const alias = this.state.domainToAliases[localDomain].splice(index, 1);
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
});
|
||||
if (this.props.canonicalAlias === alias) {
|
||||
this.setState({
|
||||
canonicalAlias: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onCanonicalAliasChange: function(event) {
|
||||
|
@ -204,12 +219,14 @@ module.exports = React.createClass({
|
|||
|
||||
let canonical_alias_section;
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
let found = false;
|
||||
canonical_alias_section = (
|
||||
<select onChange={this.onCanonicalAliasChange} defaultValue={this.state.canonicalAlias}>
|
||||
<select onChange={this.onCanonicalAliasChange} value={this.state.canonicalAlias}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(self.state.domainToAliases).map(function(domain, i) {
|
||||
return self.state.domainToAliases[domain].map(function(alias, j) {
|
||||
Object.keys(self.state.domainToAliases).map((domain, i) => {
|
||||
return self.state.domainToAliases[domain].map((alias, j) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
|
@ -218,6 +235,12 @@ module.exports = React.createClass({
|
|||
});
|
||||
})
|
||||
}
|
||||
{
|
||||
found || !this.stateCanonicalAlias ? '' :
|
||||
<option value={ this.state.canonicalAlias } key='arbitrary'>
|
||||
{ this.state.canonicalAlias }
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -90,7 +90,7 @@ module.exports = React.createClass({
|
|||
secondary_color: this.state.secondary_color,
|
||||
}).catch(function(err) {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
@ -27,8 +28,9 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
|
@ -57,6 +59,7 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
ScalarMessaging.startListening();
|
||||
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||
WidgetEchoStore.on('update', this._updateApps);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -82,6 +85,7 @@ module.exports = React.createClass({
|
|||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
WidgetEchoStore.removeListener('update', this._updateApps);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
|
@ -106,55 +110,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a URI according to a set of template variables. Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
encodeUri: function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
pathTemplate = pathTemplate.replace(
|
||||
key, encodeURIComponent(variables[key]),
|
||||
);
|
||||
}
|
||||
return pathTemplate;
|
||||
},
|
||||
|
||||
_initAppConfig: function(appId, app, sender) {
|
||||
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||
const params = {
|
||||
'$matrix_user_id': this.props.userId,
|
||||
'$matrix_room_id': this.props.room.roomId,
|
||||
'$matrix_display_name': user ? user.displayName : this.props.userId,
|
||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||
|
||||
// TODO: Namespace themes through some standard
|
||||
'$theme': SettingsStore.getValue("theme"),
|
||||
};
|
||||
|
||||
app.id = appId;
|
||||
app.name = app.name || app.type;
|
||||
|
||||
if (app.data) {
|
||||
Object.keys(app.data).forEach((key) => {
|
||||
params['$' + key] = app.data[key];
|
||||
});
|
||||
|
||||
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
||||
}
|
||||
|
||||
app.url = this.encodeUri(app.url, params);
|
||||
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
||||
|
||||
return app;
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
|
||||
return;
|
||||
|
@ -163,15 +118,11 @@ 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 this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||
const widgets = WidgetEchoStore.getEchoedRoomWidgets(
|
||||
this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room),
|
||||
);
|
||||
return widgets.map((ev) => {
|
||||
return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -219,48 +170,57 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id);
|
||||
const apps = this.state.apps.map((app, index, arr) => {
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
||||
|
||||
const apps = this.state.apps.map(
|
||||
(app, index, arr) => {
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={arr.length<2 ? true : false}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
show={this.props.showApps}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={enableScreenshots ? ["m.capability.screenshot"] : []}
|
||||
/>);
|
||||
});
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={arr.length<2 ? true : false}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
show={this.props.showApps}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
/>);
|
||||
});
|
||||
|
||||
let addWidget;
|
||||
if (this.props.showApps &&
|
||||
this._canUserModify()
|
||||
) {
|
||||
addWidget = <div
|
||||
addWidget = <AccessibleButton
|
||||
onClick={this.onClickAddWidget}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
className={this.state.apps.length<2 ?
|
||||
'mx_AddWidget_button mx_AddWidget_button_full_width' :
|
||||
'mx_AddWidget_button'
|
||||
}
|
||||
title={_t('Add a widget')}>
|
||||
[+] { _t('Add a widget') }
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let spinner;
|
||||
if (
|
||||
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
||||
this.props.room.roomId,
|
||||
WidgetUtils.getRoomWidgets(this.props.room),
|
||||
)
|
||||
) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
spinner = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
|
||||
<div id='apps' className='mx_AppsContainer'>
|
||||
{ apps }
|
||||
{ spinner }
|
||||
</div>
|
||||
{ this._canUserModify() && addWidget }
|
||||
</div>
|
||||
|
|
|
@ -216,12 +216,12 @@ export default class Autocomplete extends React.Component {
|
|||
return done.promise;
|
||||
}
|
||||
|
||||
onCompletionClicked(): boolean {
|
||||
if (this.countCompletions() === 0 || this.state.selectionOffset === COMPOSER_SELECTED) {
|
||||
onCompletionClicked(selectionOffset: number): boolean {
|
||||
if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.props.onConfirm(this.state.completionList[this.state.selectionOffset - 1]);
|
||||
this.props.onConfirm(this.state.completionList[selectionOffset - 1]);
|
||||
this.hide();
|
||||
|
||||
return true;
|
||||
|
@ -263,17 +263,14 @@ export default class Autocomplete extends React.Component {
|
|||
const componentPosition = position;
|
||||
position++;
|
||||
|
||||
const onMouseMove = () => this.setSelection(componentPosition);
|
||||
const onClick = () => {
|
||||
this.setSelection(componentPosition);
|
||||
this.onCompletionClicked();
|
||||
this.onCompletionClicked(componentPosition);
|
||||
};
|
||||
|
||||
return React.cloneElement(completion.component, {
|
||||
key: i,
|
||||
ref: `completion${position - 1}`,
|
||||
className,
|
||||
onMouseMove,
|
||||
onClick,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -47,6 +47,10 @@ const eventTileTypes = {
|
|||
};
|
||||
|
||||
const stateEventTileTypes = {
|
||||
'm.room.aliases': 'messages.TextualEvent',
|
||||
// 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex
|
||||
'm.room.canonical_alias': 'messages.TextualEvent',
|
||||
'm.room.create': 'messages.RoomCreate',
|
||||
'm.room.member': 'messages.TextualEvent',
|
||||
'm.room.name': 'messages.TextualEvent',
|
||||
'm.room.avatar': 'messages.RoomAvatarEvent',
|
||||
|
@ -56,7 +60,7 @@ const stateEventTileTypes = {
|
|||
'm.room.topic': 'messages.TextualEvent',
|
||||
'm.room.power_levels': 'messages.TextualEvent',
|
||||
'm.room.pinned_events': 'messages.TextualEvent',
|
||||
|
||||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
|
@ -273,7 +277,11 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return false;
|
||||
}
|
||||
for (let j = 0; j < rA.length; j++) {
|
||||
if (rA[j].roomMember.userId !== rB[j].roomMember.userId) {
|
||||
if (rA[j].userId !== rB[j].userId) {
|
||||
return false;
|
||||
}
|
||||
// one has a member set and the other doesn't?
|
||||
if (rA[j].roomMember !== rB[j].roomMember) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -355,7 +363,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// else set it proportional to index
|
||||
left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset;
|
||||
|
||||
const userId = receipt.roomMember.userId;
|
||||
const userId = receipt.userId;
|
||||
let readReceiptInfo;
|
||||
|
||||
if (this.props.readReceiptMap) {
|
||||
|
@ -369,6 +377,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
||||
avatars.unshift(
|
||||
<ReadReceiptMarker key={userId} member={receipt.roomMember}
|
||||
fallbackUserId={userId}
|
||||
leftOffset={left} hidden={hidden}
|
||||
readReceiptInfo={readReceiptInfo}
|
||||
checkUnmounting={this.props.checkUnmounting}
|
||||
|
@ -439,17 +448,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onPermalinkShareClicked: function(e) {
|
||||
// These permalinks are like above, can be opened in new tab/window to matrix.to
|
||||
// but otherwise fire the ShareDialog as it makes little sense to click permalink
|
||||
// whilst it is in the current room
|
||||
e.preventDefault();
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room event dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
});
|
||||
},
|
||||
|
||||
_renderE2EPadlock: function() {
|
||||
const ev = this.props.mxEvent;
|
||||
const props = {onClick: this.onCryptoClicked};
|
||||
|
@ -493,14 +491,23 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const eventType = this.props.mxEvent.getType();
|
||||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
const isInfoMessage = (eventType !== 'm.room.message' && eventType !== 'm.sticker');
|
||||
const isInfoMessage = (
|
||||
eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create'
|
||||
);
|
||||
|
||||
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
|
||||
const tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
// before trying to instantiate us
|
||||
if (!EventTileType) {
|
||||
throw new Error("Event type not supported");
|
||||
if (!tileHandler) {
|
||||
const {mxEvent} = this.props;
|
||||
console.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
|
||||
return <div className="mx_EventTile mx_EventTile_info mx_MNoticeBody">
|
||||
<div className="mx_EventTile_line">
|
||||
{ _t('This event could not be displayed') }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
const EventTileType = sdk.getComponent(tileHandler);
|
||||
|
||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||
|
@ -538,6 +545,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
if (this.props.tileShape === "notif") {
|
||||
avatarSize = 24;
|
||||
needsSenderProfile = true;
|
||||
} else if (tileHandler === 'messages.RoomCreate') {
|
||||
avatarSize = 0;
|
||||
needsSenderProfile = false;
|
||||
} else if (isInfoMessage) {
|
||||
// a small avatar, with no sender profile, for
|
||||
// joins/parts/etc
|
||||
|
@ -621,13 +631,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 }
|
||||
|
@ -680,7 +691,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_reply">
|
||||
<a href={permalink} onClick={this.onPermalinkShareClicked}>
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
|
@ -704,10 +715,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line">
|
||||
<a href={permalink} onClick={this.onPermalinkShareClicked}>
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
|
@ -721,6 +731,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ keyRequestInfo }
|
||||
{ editButton }
|
||||
</div>
|
||||
{
|
||||
// The avatar goes after the event tile as it's absolutly positioned to be over the
|
||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||
// the need for further z-indexing chaos)
|
||||
}
|
||||
{ avatar }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -742,6 +758,8 @@ module.exports.haveTileForEvent = function(e) {
|
|||
if (handler === undefined) return false;
|
||||
if (handler === 'messages.TextualEvent') {
|
||||
return TextForEvent.textForEvent(e) !== '';
|
||||
} else if (handler === 'messages.RoomCreate') {
|
||||
return Boolean(e.getContent()['predecessor']);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -332,13 +332,40 @@ module.exports = withMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMuteToggle: function() {
|
||||
_warnSelfDemote: function() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
return new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Demote yourself?"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the room it will be impossible " +
|
||||
"to regain privileges.") }
|
||||
</div>,
|
||||
button: _t("Demote"),
|
||||
onFinished: resolve,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onMuteToggle: async function() {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
// if muting self, warn as it may be irreversible
|
||||
if (target === this.props.matrixClient.getUserId()) {
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
|
@ -402,7 +429,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
console.log("Mod toggle success");
|
||||
}, function(err) {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
} else {
|
||||
console.error("Toggle moderator error:" + err);
|
||||
Modal.createTrackedDialog('Failed to toggle moderator status', '', ErrorDialog, {
|
||||
|
@ -436,7 +463,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
onPowerChange: function(powerLevel) {
|
||||
onPowerChange: async function(powerLevel) {
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
|
@ -455,20 +482,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
if (myUserId === target) {
|
||||
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
}
|
||||
},
|
||||
});
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -478,7 +497,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
|
||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||
"to have the same power level as yourself.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
|
@ -578,7 +598,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
onMemberAvatarClick: function() {
|
||||
const member = this.props.member;
|
||||
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
|
||||
const avatarUrl = member.getMxcAvatarUrl();
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
|
||||
|
@ -754,15 +774,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
for (const roomId of dmRooms) {
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(this.props.matrixClient.credentials.userId);
|
||||
|
||||
const myMembership = room.getMyMembership();
|
||||
// not a DM room if we have are not joined
|
||||
if (!me.membership || me.membership !== 'join') continue;
|
||||
// not a DM room if they are not joined
|
||||
if (myMembership !== 'join') continue;
|
||||
|
||||
const them = this.props.member;
|
||||
// not a DM room if they are not joined
|
||||
if (!them.membership || them.membership !== 'join') continue;
|
||||
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === 'invite';
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
|
@ -771,7 +791,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership === "invite"}
|
||||
isInvite={false}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
|
@ -915,7 +935,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
|
|
|
@ -32,10 +32,93 @@ module.exports = React.createClass({
|
|||
displayName: 'MemberList',
|
||||
|
||||
getInitialState: function() {
|
||||
this.memberDict = this.getMemberDict();
|
||||
const members = this.roomMembers();
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
// show an empty list
|
||||
return this._getMembersState([]);
|
||||
} else {
|
||||
return this._getMembersState(this.roomMembers());
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._mounted = true;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
cli.on("Room.myMembership", this.onMyMembership);
|
||||
} else {
|
||||
this._listenForMembersChanges();
|
||||
}
|
||||
cli.on("Room", this.onRoom); // invites & joining after peek
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||
this._showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
}
|
||||
},
|
||||
|
||||
_listenForMembersChanges: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("RoomState.events", this.onRoomStateEvent);
|
||||
// We listen for changes to the lastPresenceTs which is essentially
|
||||
// listening for all presence events (we display most of not all of
|
||||
// the information contained in presence events).
|
||||
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// cli.on("Room.timeline", this.onRoomTimeline);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._mounted = false;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
cli.removeListener("Room.myMembership", this.onMyMembership);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
cli.removeListener("Room", this.onRoom);
|
||||
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateList.cancelPendingCall();
|
||||
},
|
||||
|
||||
/**
|
||||
* If lazy loading is enabled, either:
|
||||
* show a spinner and load the members if the user is joined,
|
||||
* or show the members available so far if the user is invited
|
||||
*/
|
||||
_showMembersAccordingToMembershipWithLL: async function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
const membership = room && room.getMyMembership();
|
||||
if (membership === "join") {
|
||||
this.setState({loading: true});
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
} catch (ex) {/* already logged in RoomView */}
|
||||
if (this._mounted) {
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
this._listenForMembersChanges();
|
||||
}
|
||||
} else if (membership === "invite") {
|
||||
// show the members we've got when invited
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getMembersState: function(members) {
|
||||
// set the state after determining _showPresence to make sure it's
|
||||
// taken into account while rerendering
|
||||
return {
|
||||
loading: false,
|
||||
members: members,
|
||||
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
||||
|
@ -48,70 +131,6 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("RoomState.events", this.onRoomStateEvent);
|
||||
cli.on("Room", this.onRoom); // invites
|
||||
// We listen for changes to the lastPresenceTs which is essentially
|
||||
// listening for all presence events (we display most of not all of
|
||||
// the information contained in presence events).
|
||||
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// cli.on("Room.timeline", this.onRoomTimeline);
|
||||
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||
|
||||
this._showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
cli.removeListener("Room", this.onRoom);
|
||||
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// cli.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateList.cancelPendingCall();
|
||||
},
|
||||
|
||||
/*
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
// ignore anything but real-time updates at the end of the room:
|
||||
// updates from pagination will happen when the paginate completes.
|
||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||
|
||||
// treat any activity from a user as implicit presence to update the
|
||||
// ordering of the list whenever someone says something.
|
||||
// Except right now we're not tiebreaking "active now" users in this way
|
||||
// so don't bother for now.
|
||||
if (ev.getSender()) {
|
||||
// console.log("implicit presence from " + ev.getSender());
|
||||
|
||||
var tile = this.refs[ev.getSender()];
|
||||
if (tile) {
|
||||
// work around a race where you might have a room member object
|
||||
// before the user object exists. XXX: why does this ever happen?
|
||||
var all_members = room.currentState.members;
|
||||
var userId = ev.getSender();
|
||||
if (all_members[userId].user === null) {
|
||||
all_members[userId].user = MatrixClientPeg.get().getUser(userId);
|
||||
}
|
||||
this._updateList(); // reorder the membership list
|
||||
}
|
||||
}
|
||||
},
|
||||
*/
|
||||
|
||||
onUserLastPresenceTs(event, user) {
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
|
@ -130,28 +149,40 @@ module.exports = React.createClass({
|
|||
// We listen for room events because when we accept an invite
|
||||
// we need to wait till the room is fully populated with state
|
||||
// before refreshing the member list else we get a stale list.
|
||||
this._updateList();
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
},
|
||||
|
||||
onMyMembership: function(room, membership, oldMembership) {
|
||||
if (room.roomId === this.props.roomId && membership === "join") {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
onRoomMemberName: function(ev, member) {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
onRoomStateEvent: function(event, state) {
|
||||
if (event.getType() === "m.room.third_party_invite") {
|
||||
if (event.getRoomId() === this.props.roomId &&
|
||||
event.getType() === "m.room.third_party_invite") {
|
||||
this._updateList();
|
||||
}
|
||||
},
|
||||
|
||||
_updateList: new rate_limited_func(function() {
|
||||
// console.log("Updating memberlist");
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
const newState = {
|
||||
loading: false,
|
||||
members: this.roomMembers(),
|
||||
};
|
||||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
|
||||
|
@ -159,50 +190,43 @@ module.exports = React.createClass({
|
|||
this.setState(newState);
|
||||
}, 500),
|
||||
|
||||
getMemberDict: function() {
|
||||
if (!this.props.roomId) return {};
|
||||
getMembersWithUser: function() {
|
||||
if (!this.props.roomId) return [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
if (!room) return {};
|
||||
if (!room) return [];
|
||||
|
||||
const all_members = room.currentState.members;
|
||||
const allMembers = Object.values(room.currentState.members);
|
||||
|
||||
Object.keys(all_members).map(function(userId) {
|
||||
allMembers.forEach(function(member) {
|
||||
// work around a race where you might have a room member object
|
||||
// before the user object exists. This may or may not cause
|
||||
// https://github.com/vector-im/vector-web/issues/186
|
||||
if (all_members[userId].user === null) {
|
||||
all_members[userId].user = MatrixClientPeg.get().getUser(userId);
|
||||
if (member.user === null) {
|
||||
member.user = cli.getUser(member.userId);
|
||||
}
|
||||
|
||||
// XXX: this user may have no lastPresenceTs value!
|
||||
// the right solution here is to fix the race rather than leave it as 0
|
||||
});
|
||||
|
||||
return all_members;
|
||||
return allMembers;
|
||||
},
|
||||
|
||||
roomMembers: function() {
|
||||
const all_members = this.memberDict || {};
|
||||
const all_user_ids = Object.keys(all_members);
|
||||
const ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
|
||||
all_user_ids.sort(this.memberSort);
|
||||
|
||||
const to_display = [];
|
||||
let count = 0;
|
||||
for (let i = 0; i < all_user_ids.length; ++i) {
|
||||
const user_id = all_user_ids[i];
|
||||
const m = all_members[user_id];
|
||||
|
||||
if (m.membership === 'join' || m.membership === 'invite') {
|
||||
if ((ConferenceHandler && !ConferenceHandler.isConferenceUser(user_id)) || !ConferenceHandler) {
|
||||
to_display.push(user_id);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return to_display;
|
||||
const allMembers = this.getMembersWithUser();
|
||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||
return (
|
||||
m.membership === 'join' || m.membership === 'invite'
|
||||
) && (
|
||||
!ConferenceHandler ||
|
||||
(ConferenceHandler && !ConferenceHandler.isConferenceUser(m.userId))
|
||||
);
|
||||
});
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
},
|
||||
|
||||
_createOverflowTileJoined: function(overflowCount, totalCount) {
|
||||
|
@ -249,14 +273,12 @@ module.exports = React.createClass({
|
|||
// returns negative if a comes before b,
|
||||
// returns 0 if a and b are equivalent in ordering
|
||||
// returns positive if a comes after b.
|
||||
memberSort: function(userIdA, userIdB) {
|
||||
memberSort: function(memberA, memberB) {
|
||||
// order by last active, with "active now" first.
|
||||
// ...and then by power
|
||||
// ...and then alphabetically.
|
||||
// We could tiebreak instead by "last recently spoken in this room" if we wanted to.
|
||||
|
||||
const memberA = this.memberDict[userIdA];
|
||||
const memberB = this.memberDict[userIdB];
|
||||
const userA = memberA.user;
|
||||
const userB = memberB.user;
|
||||
|
||||
|
@ -306,9 +328,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_filterMembers: function(members, membership, query) {
|
||||
return members.filter((userId) => {
|
||||
const m = this.memberDict[userId];
|
||||
|
||||
return members.filter((m) => {
|
||||
if (query) {
|
||||
query = query.toLowerCase();
|
||||
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
|
||||
|
@ -350,10 +370,9 @@ module.exports = React.createClass({
|
|||
_makeMemberTiles: function(members, membership) {
|
||||
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
|
||||
const memberList = members.map((userId) => {
|
||||
const m = this.memberDict[userId];
|
||||
const memberList = members.map((m) => {
|
||||
return (
|
||||
<MemberTile key={userId} member={m} ref={userId} showPresence={this._showPresence} />
|
||||
<MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -393,6 +412,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <div className="mx_MemberList"><Spinner /></div>;
|
||||
}
|
||||
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -25,6 +25,18 @@ import dis from '../../../dispatcher';
|
|||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink } from '../../../matrix-to';
|
||||
|
||||
const formatButtonList = [
|
||||
_td("bold"),
|
||||
_td("italic"),
|
||||
_td("deleted"),
|
||||
_td("underlined"),
|
||||
_td("inline-code"),
|
||||
_td("block-quote"),
|
||||
_td("bulleted-list"),
|
||||
_td("numbered-list"),
|
||||
];
|
||||
|
||||
export default class MessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
|
@ -35,25 +47,24 @@ export default class MessageComposer extends React.Component {
|
|||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
this.onInputContentChanged = this.onInputContentChanged.bind(this);
|
||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
||||
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
|
||||
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
|
||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||
this.onEvent = this.onEvent.bind(this);
|
||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
autocompleteQuery: '',
|
||||
selection: null,
|
||||
inputState: {
|
||||
style: [],
|
||||
marks: [],
|
||||
blockType: null,
|
||||
isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
||||
wordCount: 0,
|
||||
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
||||
},
|
||||
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -63,12 +74,31 @@ export default class MessageComposer extends React.Component {
|
|||
// marked as encrypted.
|
||||
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
||||
MatrixClientPeg.get().on("event", this.onEvent);
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._waitForOwnMember();
|
||||
}
|
||||
|
||||
_waitForOwnMember() {
|
||||
// if we have the member already, do that
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
if (me) {
|
||||
this.setState({me});
|
||||
return;
|
||||
}
|
||||
// Otherwise, wait for member loading to finish and then update the member for the avatar.
|
||||
// The members should already be loading, and loadMembersIfNeeded
|
||||
// will return the promise for the existing operation
|
||||
this.props.room.loadMembersIfNeeded().then(() => {
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
this.setState({me});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("event", this.onEvent);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
|
@ -81,6 +111,18 @@ export default class MessageComposer extends React.Component {
|
|||
this.forceUpdate();
|
||||
}
|
||||
|
||||
_onRoomStateEvents(ev, state) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId) return;
|
||||
|
||||
if (ev.getType() === 'm.room.tombstone') {
|
||||
this.setState({tombstone: this._getRoomTombstone()});
|
||||
}
|
||||
}
|
||||
|
||||
_getRoomTombstone() {
|
||||
return this.props.room.currentState.getStateEvents('m.room.tombstone', '');
|
||||
}
|
||||
|
||||
_onRoomViewStoreUpdate() {
|
||||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||
if (this.state.isQuoting === isQuoting) return;
|
||||
|
@ -89,7 +131,7 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -211,13 +253,6 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
this.setState({
|
||||
autocompleteQuery: content,
|
||||
selection,
|
||||
});
|
||||
}
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
this.setState({inputState});
|
||||
}
|
||||
|
@ -228,7 +263,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", event) {
|
||||
onFormatButtonClicked(name, event) {
|
||||
event.preventDefault();
|
||||
this.messageComposerInput.onFormatButtonClicked(name, event);
|
||||
}
|
||||
|
@ -240,11 +275,21 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
onToggleMarkdownClicked(e) {
|
||||
e.preventDefault(); // don't steal focus from the editor!
|
||||
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
|
||||
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled);
|
||||
}
|
||||
|
||||
_onTombstoneClick(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
highlighted: true,
|
||||
room_id: replacementRoomId,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
@ -252,11 +297,13 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
const controls = [];
|
||||
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={me} width={24} height={24} />
|
||||
</div>,
|
||||
);
|
||||
if (this.state.me) {
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={this.state.me} width={24} height={24} />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
let e2eImg, e2eTitle, e2eClass;
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
|
@ -281,49 +328,51 @@ export default class MessageComposer extends React.Component {
|
|||
let videoCallButton;
|
||||
let hangupButton;
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
// Call buttons
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
hangupButton =
|
||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
<AccessibleButton key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
<img src="img/hangup.svg" alt={_t('Hangup')} title={_t('Hangup')} width="25" height="26" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
callButton =
|
||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
|
||||
<AccessibleButton key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
|
||||
<TintableSvg src="img/icon-call.svg" width="35" height="35" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
videoCallButton =
|
||||
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
|
||||
<AccessibleButton key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
|
||||
<TintableSvg src="img/icons-video.svg" width="35" height="35" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
MatrixClientPeg.get().credentials.userId);
|
||||
const canSendMessages = !this.state.tombstone &&
|
||||
this.props.room.maySendMessage();
|
||||
|
||||
if (canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
const uploadButton = (
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
<AccessibleButton key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title={_t('Upload file')}>
|
||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35" />
|
||||
<input ref="uploadInput" type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onChange={this.onUploadFileSelected} />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
const formattingButton = (
|
||||
<img className="mx_MessageComposer_formatting"
|
||||
const formattingButton = this.state.inputState.isRichTextEnabled ? (
|
||||
<AccessibleButton element="img" className="mx_MessageComposer_formatting"
|
||||
alt={_t("Show Text Formatting Toolbar")}
|
||||
title={_t("Show Text Formatting Toolbar")}
|
||||
src="img/button-text-formatting.svg"
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
|
||||
key="controls_formatting" />
|
||||
);
|
||||
) : null;
|
||||
|
||||
let placeholderText;
|
||||
if (this.state.isQuoting) {
|
||||
|
@ -350,7 +399,6 @@ export default class MessageComposer extends React.Component {
|
|||
room={this.props.room}
|
||||
placeholder={placeholderText}
|
||||
onFilesPasted={this.uploadFiles}
|
||||
onContentChanged={this.onInputContentChanged}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
formattingButton,
|
||||
stickerpickerButton,
|
||||
|
@ -359,6 +407,23 @@ export default class MessageComposer extends React.Component {
|
|||
callButton,
|
||||
videoCallButton,
|
||||
);
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<img className="mx_MessageComposer_roomReplaced_icon" src="img/room_replaced.svg" />
|
||||
<span className="mx_MessageComposer_roomReplaced_header">
|
||||
{_t("This room has been replaced and is no longer active.")}
|
||||
</span><br />
|
||||
<a href={makeRoomPermalink(replacementRoomId)}
|
||||
className="mx_MessageComposer_roomReplaced_link"
|
||||
onClick={this._onTombstoneClick}
|
||||
>
|
||||
{_t("The conversation continues here.")}
|
||||
</a>
|
||||
</div>
|
||||
</div>);
|
||||
} else {
|
||||
controls.push(
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
|
@ -367,11 +432,14 @@ export default class MessageComposer extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const {style, blockType} = this.state.inputState;
|
||||
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
||||
(name) => {
|
||||
const active = style.includes(name) || blockType === name;
|
||||
const suffix = active ? '-o-n' : '';
|
||||
let formatBar;
|
||||
if (this.state.showFormatting && this.state.inputState.isRichTextEnabled) {
|
||||
const {marks, blockType} = this.state.inputState;
|
||||
const formatButtons = formatButtonList.map((name) => {
|
||||
// special-case to match the md serializer and the special-case in MessageComposerInput.js
|
||||
const markName = name === 'inline-code' ? 'code' : name;
|
||||
const active = marks.some(mark => mark.type === markName) || blockType === name;
|
||||
const suffix = active ? '-on' : '';
|
||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
|
||||
return <img className={className}
|
||||
|
@ -380,8 +448,25 @@ export default class MessageComposer extends React.Component {
|
|||
key={name}
|
||||
src={`img/button-text-${name}${suffix}.svg`}
|
||||
height="17" />;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
formatBar =
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar">
|
||||
{ formatButtons }
|
||||
<div style={{flex: 1}}></div>
|
||||
<img title={this.state.inputState.isRichTextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={`img/button-md-${!this.state.inputState.isRichTextEnabled}.png`} />
|
||||
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer">
|
||||
|
@ -390,20 +475,7 @@ export default class MessageComposer extends React.Component {
|
|||
{ controls }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
|
||||
{ formatButtons }
|
||||
<div style={{flex: 1}}></div>
|
||||
<img title={this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
||||
<img title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
</div>
|
||||
</div>
|
||||
{ formatBar }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -41,7 +41,10 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// the RoomMember to show the RR for
|
||||
member: PropTypes.object.isRequired,
|
||||
member: PropTypes.object,
|
||||
// userId to fallback the avatar to
|
||||
// if the member hasn't been loaded yet
|
||||
fallbackUserId: PropTypes.string.isRequired,
|
||||
|
||||
// number of pixels to offset the avatar from the right of its parent;
|
||||
// typically a negative value.
|
||||
|
@ -130,8 +133,7 @@ module.exports = React.createClass({
|
|||
// the docs for `offsetParent` say it may be null if `display` is
|
||||
// `none`, but I can't see why that would happen.
|
||||
console.warn(
|
||||
`ReadReceiptMarker for ${this.props.member.userId} in ` +
|
||||
`${this.props.member.roomId} has no offsetParent`,
|
||||
`ReadReceiptMarker for ${this.props.fallbackUserId} in has no offsetParent`,
|
||||
);
|
||||
startTopOffset = 0;
|
||||
} else {
|
||||
|
@ -186,17 +188,17 @@ module.exports = React.createClass({
|
|||
let title;
|
||||
if (this.props.timestamp) {
|
||||
const dateString = formatDate(new Date(this.props.timestamp), this.props.showTwelveHour);
|
||||
if (this.props.member.userId === this.props.member.rawDisplayName) {
|
||||
if (!this.props.member || this.props.fallbackUserId === this.props.member.rawDisplayName) {
|
||||
title = _t(
|
||||
"Seen by %(userName)s at %(dateTime)s",
|
||||
{userName: this.props.member.userId,
|
||||
{userName: this.props.fallbackUserId,
|
||||
dateTime: dateString},
|
||||
);
|
||||
} else {
|
||||
title = _t(
|
||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
||||
{displayName: this.props.member.rawDisplayName,
|
||||
userName: this.props.member.userId,
|
||||
userName: this.props.fallbackUserId,
|
||||
dateTime: dateString},
|
||||
);
|
||||
}
|
||||
|
@ -208,6 +210,7 @@ module.exports = React.createClass({
|
|||
enterTransitionOpts={this.state.enterTransitionOpts} >
|
||||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
fallbackUserId={this.props.fallbackUserId}
|
||||
aria-hidden="true"
|
||||
width={14} height={14} resizeMethod="crop"
|
||||
style={style}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomDropTarget',
|
||||
|
@ -31,5 +31,5 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 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.
|
||||
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
'use strict';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -33,7 +35,12 @@ import RoomListStore from '../../../stores/RoomListStore';
|
|||
import GroupStore from '../../../stores/GroupStore';
|
||||
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
|
||||
function labelForTagName(tagName) {
|
||||
if (tagName.startsWith('u.')) return tagName.slice(2);
|
||||
return tagName;
|
||||
}
|
||||
|
||||
function phraseForSection(section) {
|
||||
switch (section) {
|
||||
|
@ -90,7 +97,7 @@ module.exports = React.createClass({
|
|||
};
|
||||
// All rooms that should be kept in the room list when filtering.
|
||||
// By default, show all rooms.
|
||||
this._visibleRooms = MatrixClientPeg.get().getRooms();
|
||||
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
|
||||
|
||||
// Listen to updates to group data. RoomList cares about members and rooms in order
|
||||
// to filter the room list when group tags are selected.
|
||||
|
@ -295,7 +302,7 @@ module.exports = React.createClass({
|
|||
this._visibleRooms = Array.from(roomSet);
|
||||
} else {
|
||||
// Show all rooms
|
||||
this._visibleRooms = MatrixClientPeg.get().getRooms();
|
||||
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
|
||||
}
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
@ -340,8 +347,8 @@ module.exports = React.createClass({
|
|||
if (!taggedRoom) {
|
||||
return;
|
||||
}
|
||||
const me = taggedRoom.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, me, this.props.ConferenceHandler)) {
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, myUserId, this.props.ConferenceHandler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -444,6 +451,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.stickies) return;
|
||||
|
||||
const self = this;
|
||||
let scrollStuckOffset = 0;
|
||||
// Scroll to the passed in position, i.e. a header was clicked and in a scroll to state
|
||||
|
@ -608,10 +617,14 @@ module.exports = React.createClass({
|
|||
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
// XXX: we can't detect device-level (localStorage) settings onChange as the SettingsStore does not notify
|
||||
// so checking on every render is the sanest thing at this time.
|
||||
const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty');
|
||||
|
||||
const self = this;
|
||||
return (
|
||||
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
|
||||
autoshow={true} onScroll={self._whenScrolling} onResize={self._whenScrolling} wrappedRef={this._collectGemini}>
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={[]}
|
||||
extraTiles={this._makeGroupInviteTiles(self.props.searchFilter)}
|
||||
|
@ -623,6 +636,7 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||
|
@ -635,6 +649,7 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['m.favourite']}
|
||||
|
@ -647,7 +662,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
|
||||
label={_t('People')}
|
||||
|
@ -661,7 +677,8 @@ module.exports = React.createClass({
|
|||
alwaysShowHeader={true}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
|
||||
label={_t('Rooms')}
|
||||
|
@ -673,13 +690,14 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
{ Object.keys(self.state.lists).map((tagName) => {
|
||||
if (!tagName.match(STANDARD_TAGS_REGEX)) {
|
||||
return <RoomSubList list={self.state.lists[tagName]}
|
||||
key={tagName}
|
||||
label={tagName}
|
||||
label={labelForTagName(tagName)}
|
||||
tagName={tagName}
|
||||
emptyContent={this._getEmptyContent(tagName)}
|
||||
editable={true}
|
||||
|
@ -688,7 +706,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />;
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />;
|
||||
}
|
||||
}) }
|
||||
|
||||
|
@ -702,9 +721,17 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
|
||||
emptyContent={self.props.collapsed ? null :
|
||||
<div className="mx_RoomList_emptySubListTip_container">
|
||||
<div className="mx_RoomList_emptySubListTip">
|
||||
{ _t('You have no historical rooms') }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
label={_t('Historical')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
|
@ -712,10 +739,23 @@ module.exports = React.createClass({
|
|||
alwaysShowHeader={true}
|
||||
startAsHidden={true}
|
||||
showSpinner={self.state.isLoadingLeftRooms}
|
||||
onHeaderClick= {self.onArchivedHeaderClick}
|
||||
onHeaderClick={self.onArchivedHeaderClick}
|
||||
incomingCall={self.state.incomingCall}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['m.server_notice']}
|
||||
label={_t('System Alerts')}
|
||||
tagName="m.lowpriority"
|
||||
editable={false}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={false} />
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
|
|
|
@ -98,15 +98,11 @@ module.exports = React.createClass({
|
|||
</div>);
|
||||
}
|
||||
|
||||
const myMember = this.props.room ? this.props.room.currentState.members[
|
||||
MatrixClientPeg.get().credentials.userId
|
||||
] : null;
|
||||
const kicked = (
|
||||
myMember &&
|
||||
myMember.membership == 'leave' &&
|
||||
myMember.events.member.getSender() != MatrixClientPeg.get().credentials.userId
|
||||
);
|
||||
const banned = myMember && myMember.membership == 'ban';
|
||||
const myMember = this.props.room ?
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId()) :
|
||||
null;
|
||||
const kicked = myMember && myMember.isKicked();
|
||||
const banned = myMember && myMember && myMember.membership == 'ban';
|
||||
|
||||
if (this.props.inviterName) {
|
||||
let emailMatchBlock;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -571,6 +572,11 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onRoomUpgradeClick: function() {
|
||||
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: this.props.room});
|
||||
},
|
||||
|
||||
_onRoomMemberMembership: function() {
|
||||
// Update, since our banned user list may have changed
|
||||
this.forceUpdate();
|
||||
|
@ -793,15 +799,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
let leaveButton = null;
|
||||
const myMember = this.props.room.getMember(myUserId);
|
||||
if (myMember) {
|
||||
if (myMember.membership === "join") {
|
||||
const myMemberShip = this.props.room.getMyMembership();
|
||||
if (myMemberShip) {
|
||||
if (myMemberShip === "join") {
|
||||
leaveButton = (
|
||||
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onLeaveClick}>
|
||||
{ _t('Leave room') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (myMember.membership === "leave") {
|
||||
} else if (myMemberShip === "leave") {
|
||||
leaveButton = (
|
||||
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onForgetClick}>
|
||||
{ _t('Forget room') }
|
||||
|
@ -929,6 +935,13 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
|
||||
let roomUpgradeButton = null;
|
||||
if (this.props.room.shouldUpgradeToVersion() && this.props.room.userMayUpgradeRoom(myUserId)) {
|
||||
roomUpgradeButton = <AccessibleButton className="mx_RoomSettings_upgradeButton danger" onClick={this._onRoomUpgradeClick}>
|
||||
{ _t("Upgrade room to version %(ver)s", {ver: this.props.room.shouldUpgradeToVersion()}) }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings">
|
||||
|
||||
|
@ -1039,7 +1052,9 @@ module.exports = React.createClass({
|
|||
|
||||
<h3>{ _t('Advanced') }</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
{ _t('This room\'s internal ID is') } <code>{ this.props.room.roomId }</code>
|
||||
{ _t('Internal room ID: ') } <code>{ this.props.room.roomId }</code><br />
|
||||
{ _t('Room version number: ') } <code>{ this.props.room.getVersion() }</code><br />
|
||||
{ roomUpgradeButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -243,9 +243,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const me = this.props.room.currentState.members[myUserId];
|
||||
|
||||
const isInvite = this.props.room.getMyMembership() === "invite";
|
||||
const notificationCount = this.state.notificationCount;
|
||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||
|
||||
|
@ -259,7 +257,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': isInvite,
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
|
@ -275,6 +273,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
let name = this.state.roomName;
|
||||
if (name == undefined || name == null) name = '';
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
let badgeContent;
|
||||
|
|
57
src/components/views/rooms/RoomUpgradeWarningBar.js
Normal file
57
src/components/views/rooms/RoomUpgradeWarningBar.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
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 sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomUpgradeWarningBar',
|
||||
|
||||
propTypes: {
|
||||
room: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
onUpgradeClick: function() {
|
||||
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: this.props.room});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_RoomUpgradeWarningBar">
|
||||
<div className="mx_RoomUpgradeWarningBar_header">
|
||||
{_t("There is a known vulnerability affecting this room.")}
|
||||
</div>
|
||||
<div className="mx_RoomUpgradeWarningBar_body">
|
||||
{_t("This room version is vulnerable to malicious modification of room state.")}
|
||||
</div>
|
||||
<p className="mx_RoomUpgradeWarningBar_upgradelink">
|
||||
<AccessibleButton onClick={this.onUpgradeClick}>
|
||||
{_t("Click here to upgrade to the latest room version and ensure room integrity is protected.")}
|
||||
</AccessibleButton>
|
||||
</p>
|
||||
<div className="mx_RoomUpgradeWarningBar_small">
|
||||
{_t("Only room administrators will see this warning")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -16,11 +16,11 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
var classNames = require('classnames');
|
||||
var AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
const React = require('react');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require('../../../index');
|
||||
const classNames = require('classnames');
|
||||
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -28,7 +28,7 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return ({
|
||||
scope: 'Room'
|
||||
scope: 'Room',
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -54,18 +54,18 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var searchButtonClasses = classNames({ mx_SearchBar_searchButton : true, mx_SearchBar_searching: this.props.searchInProgress });
|
||||
var thisRoomClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'Room' });
|
||||
var allRoomsClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'All' });
|
||||
const searchButtonClasses = classNames({ mx_SearchBar_searchButton: true, mx_SearchBar_searching: this.props.searchInProgress });
|
||||
const thisRoomClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'Room' });
|
||||
const allRoomsClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'All' });
|
||||
|
||||
return (
|
||||
<div className="mx_SearchBar">
|
||||
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange}/>
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")}/></AccessibleButton>
|
||||
<div className="mx_SearchBar">
|
||||
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")} /></AccessibleButton>
|
||||
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick}>{_t("This Room")}</AccessibleButton>
|
||||
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick}>{_t("All Rooms")}</AccessibleButton>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Widgets from '../../../utils/widgets';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -24,9 +23,15 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
|
||||
// We sit in a context menu, so the persisted element container needs to float
|
||||
// above it, so it needs a greater z-index than the ContextMenu
|
||||
const STICKERPICKER_Z_INDEX = 5000;
|
||||
|
||||
export default class Stickerpicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -39,8 +44,6 @@ export default class Stickerpicker extends React.Component {
|
|||
this._onResize = this._onResize.bind(this);
|
||||
this._onFinished = this._onFinished.bind(this);
|
||||
|
||||
this._collectWidgetMessaging = this._collectWidgetMessaging.bind(this);
|
||||
|
||||
this.popoverWidth = 300;
|
||||
this.popoverHeight = 300;
|
||||
|
||||
|
@ -67,7 +70,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 +122,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,
|
||||
|
@ -148,8 +151,8 @@ export default class Stickerpicker extends React.Component {
|
|||
<AccessibleButton onClick={this._launchManageIntegrations}
|
||||
className='mx_Stickers_contentPlaceholder'>
|
||||
<p>{ _t("You don't currently have any stickerpacks enabled") }</p>
|
||||
<p className='mx_Stickers_addLink'>Add some now</p>
|
||||
<img src='img/stickerpack-placeholder.png' alt={_t('Add a stickerpack')} />
|
||||
<p className='mx_Stickers_addLink'>{ _t("Add some now") }</p>
|
||||
<img src='img/stickerpack-placeholder.png' alt="" />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -162,17 +165,11 @@ export default class Stickerpicker extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_collectWidgetMessaging(widgetMessaging) {
|
||||
this._appWidgetMessaging = widgetMessaging;
|
||||
|
||||
// Do this now instead of in componentDidMount because we might not have had the
|
||||
// reference to widgetMessaging when mounting
|
||||
this._sendVisibilityToWidget(true);
|
||||
}
|
||||
|
||||
_sendVisibilityToWidget(visible) {
|
||||
if (this._appWidgetMessaging && visible !== this._prevSentVisibility) {
|
||||
this._appWidgetMessaging.sendVisibility(visible);
|
||||
if (!this.state.stickerpickerWidget) return;
|
||||
const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
|
||||
if (widgetMessaging && visible !== this._prevSentVisibility) {
|
||||
widgetMessaging.sendVisibility(visible);
|
||||
this._prevSentVisibility = visible;
|
||||
}
|
||||
}
|
||||
|
@ -211,9 +208,8 @@ export default class Stickerpicker extends React.Component {
|
|||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement>
|
||||
<PersistedElement persistKey="stickerPicker" style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<AppTile
|
||||
collectWidgetMessaging={this._collectWidgetMessaging}
|
||||
id={stickerpickerWidget.id}
|
||||
url={stickerpickerWidget.content.url}
|
||||
name={stickerpickerWidget.content.name}
|
||||
|
@ -348,7 +344,7 @@ export default class Stickerpicker extends React.Component {
|
|||
if (this.state.showStickers) {
|
||||
// Show hide-stickers button
|
||||
stickersButton =
|
||||
<div
|
||||
<AccessibleButton
|
||||
id='stickersButton'
|
||||
key="controls_hide_stickers"
|
||||
className="mx_MessageComposer_stickers mx_Stickers_hideStickers"
|
||||
|
@ -356,18 +352,18 @@ export default class Stickerpicker extends React.Component {
|
|||
ref='target'
|
||||
title={_t("Hide Stickers")}>
|
||||
<TintableSvg src="img/icons-hide-stickers.svg" width="35" height="35" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
// Show show-stickers button
|
||||
stickersButton =
|
||||
<div
|
||||
<AccessibleButton
|
||||
id='stickersButton'
|
||||
key="constrols_show_stickers"
|
||||
key="controls_show_stickers"
|
||||
className="mx_MessageComposer_stickers"
|
||||
onClick={this._onShowStickersClick}
|
||||
title={_t("Show Stickers")}>
|
||||
<TintableSvg src="img/icons-show-stickers.svg" width="35" height="35" />
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
return <div>
|
||||
{stickersButton}
|
||||
|
|
|
@ -164,6 +164,7 @@ export default class DevicesPanel extends React.Component {
|
|||
|
||||
render() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (this.state.deviceLoadError !== undefined) {
|
||||
const classes = classNames(this.props.className, "error");
|
||||
|
@ -185,9 +186,9 @@ export default class DevicesPanel extends React.Component {
|
|||
|
||||
const deleteButton = this.state.deleting ?
|
||||
<Spinner w={22} h={22} /> :
|
||||
<div className="mx_textButton" onClick={this._onDeleteClick}>
|
||||
<AccessibleButton className="mx_textButton" onClick={this._onDeleteClick}>
|
||||
{ _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) }
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
|
||||
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||
return (
|
||||
|
|
|
@ -16,10 +16,10 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var dis = require('../../../dispatcher');
|
||||
const React = require('react');
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const dis = require('../../../dispatcher');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'IntegrationsManager',
|
||||
|
@ -59,5 +59,5 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<iframe src={ this.props.src }></iframe>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
NotificationUtils,
|
||||
VectorPushRulesDefinitions,
|
||||
PushRuleVectorState,
|
||||
ContentRules
|
||||
ContentRules,
|
||||
} from '../../../notifications';
|
||||
|
||||
// TODO: this "view" component still has far too much application logic in it,
|
||||
|
@ -47,7 +47,7 @@ const LEGACY_RULES = {
|
|||
"im.vector.rule.room_message": ".m.rule.message",
|
||||
"im.vector.rule.invite_for_me": ".m.rule.invite_for_me",
|
||||
"im.vector.rule.call": ".m.rule.call",
|
||||
"im.vector.rule.notices": ".m.rule.suppress_notices"
|
||||
"im.vector.rule.notices": ".m.rule.suppress_notices",
|
||||
};
|
||||
|
||||
function portLegacyActions(actions) {
|
||||
|
@ -67,7 +67,7 @@ module.exports = React.createClass({
|
|||
phases: {
|
||||
LOADING: "LOADING", // The component is loading or sending data to the hs
|
||||
DISPLAY: "DISPLAY", // The component is ready and display data
|
||||
ERROR: "ERROR" // There was an error
|
||||
ERROR: "ERROR", // There was an error
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
|
@ -79,7 +79,7 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
threepids: []
|
||||
threepids: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -90,10 +90,10 @@ module.exports = React.createClass({
|
|||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
||||
vectorState: PushRuleVectorState.ON,
|
||||
rules: []
|
||||
rules: [],
|
||||
},
|
||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
||||
externalContentRules: [] // Keyword push rules that have been defined outside Vector UI
|
||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -104,7 +104,7 @@ module.exports = React.createClass({
|
|||
onEnableNotificationsChange: function(event) {
|
||||
const self = this;
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
phase: this.phases.LOADING,
|
||||
});
|
||||
|
||||
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !event.target.checked).done(function() {
|
||||
|
@ -145,7 +145,7 @@ module.exports = React.createClass({
|
|||
onEnableEmailNotificationsChange: function(address, event) {
|
||||
let emailPusherPromise;
|
||||
if (event.target.checked) {
|
||||
const data = {}
|
||||
const data = {};
|
||||
data['brand'] = this.props.brand || 'Riot';
|
||||
emailPusherPromise = UserSettingsStore.addEmailPusher(address, data);
|
||||
} else {
|
||||
|
@ -170,9 +170,8 @@ module.exports = React.createClass({
|
|||
const newPushRuleVectorState = event.target.className.split("-")[1];
|
||||
|
||||
if ("_keywords" === vectorRuleId) {
|
||||
this._setKeywordsPushRuleVectorState(newPushRuleVectorState)
|
||||
}
|
||||
else {
|
||||
this._setKeywordsPushRuleVectorState(newPushRuleVectorState);
|
||||
} else {
|
||||
const rule = this.getRule(vectorRuleId);
|
||||
if (rule) {
|
||||
this._setPushRuleVectorState(rule, newPushRuleVectorState);
|
||||
|
@ -185,7 +184,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Compute the keywords list to display
|
||||
let keywords = [];
|
||||
for (let i in this.state.vectorContentRules.rules) {
|
||||
for (const i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
keywords.push(rule.pattern);
|
||||
}
|
||||
|
@ -195,8 +194,7 @@ module.exports = React.createClass({
|
|||
keywords.sort();
|
||||
|
||||
keywords = keywords.join(", ");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
keywords = "";
|
||||
}
|
||||
|
||||
|
@ -207,29 +205,28 @@ module.exports = React.createClass({
|
|||
button: _t('OK'),
|
||||
value: keywords,
|
||||
onFinished: function onFinished(should_leave, newValue) {
|
||||
|
||||
if (should_leave && newValue !== keywords) {
|
||||
let newKeywords = newValue.split(',');
|
||||
for (let i in newKeywords) {
|
||||
for (const i in newKeywords) {
|
||||
newKeywords[i] = newKeywords[i].trim();
|
||||
}
|
||||
|
||||
// Remove duplicates and empty
|
||||
newKeywords = newKeywords.reduce(function(array, keyword){
|
||||
newKeywords = newKeywords.reduce(function(array, keyword) {
|
||||
if (keyword !== "" && array.indexOf(keyword) < 0) {
|
||||
array.push(keyword);
|
||||
}
|
||||
return array;
|
||||
},[]);
|
||||
}, []);
|
||||
|
||||
self._setKeywords(newKeywords);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
getRule: function(vectorRuleId) {
|
||||
for (let i in this.state.vectorPushRules) {
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
if (rule.vectorRuleId === vectorRuleId) {
|
||||
return rule;
|
||||
|
@ -239,9 +236,8 @@ module.exports = React.createClass({
|
|||
|
||||
_setPushRuleVectorState: function(rule, newPushRuleVectorState) {
|
||||
if (rule && rule.vectorState !== newPushRuleVectorState) {
|
||||
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
phase: this.phases.LOADING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
@ -255,8 +251,7 @@ module.exports = React.createClass({
|
|||
if (!actions) {
|
||||
// The new state corresponds to disabling the rule.
|
||||
deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// The new state corresponds to enabling the rule and setting specific actions
|
||||
deferreds.push(this._updatePushRuleActions(rule.rule, actions, true));
|
||||
}
|
||||
|
@ -270,7 +265,7 @@ module.exports = React.createClass({
|
|||
Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, {
|
||||
title: _t('Failed to change settings'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -287,12 +282,12 @@ module.exports = React.createClass({
|
|||
const cli = MatrixClientPeg.get();
|
||||
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
phase: this.phases.LOADING,
|
||||
});
|
||||
|
||||
// Update all rules in self.state.vectorContentRules
|
||||
const deferreds = [];
|
||||
for (let i in this.state.vectorContentRules.rules) {
|
||||
for (const i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
|
||||
let enabled, actions;
|
||||
|
@ -326,8 +321,7 @@ module.exports = React.createClass({
|
|||
// Note that the workaround in _updatePushRuleActions will automatically
|
||||
// enable the rule
|
||||
deferreds.push(this._updatePushRuleActions(rule, actions, enabled));
|
||||
}
|
||||
else if (enabled != undefined) {
|
||||
} else if (enabled != undefined) {
|
||||
deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled));
|
||||
}
|
||||
}
|
||||
|
@ -340,14 +334,14 @@ module.exports = React.createClass({
|
|||
Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, {
|
||||
title: _t('Can\'t update user notification settings'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_setKeywords: function(newKeywords) {
|
||||
this.setState({
|
||||
phase: this.phases.LOADING
|
||||
phase: this.phases.LOADING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
@ -356,7 +350,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Remove per-word push rules of keywords that are no more in the list
|
||||
const vectorContentRulesPatterns = [];
|
||||
for (let i in self.state.vectorContentRules.rules) {
|
||||
for (const i in self.state.vectorContentRules.rules) {
|
||||
const rule = self.state.vectorContentRules.rules[i];
|
||||
|
||||
vectorContentRulesPatterns.push(rule.pattern);
|
||||
|
@ -368,7 +362,7 @@ module.exports = React.createClass({
|
|||
|
||||
// If the keyword is part of `externalContentRules`, remove the rule
|
||||
// before recreating it in the right Vector path
|
||||
for (let i in self.state.externalContentRules) {
|
||||
for (const i in self.state.externalContentRules) {
|
||||
const rule = self.state.externalContentRules[i];
|
||||
|
||||
if (newKeywords.indexOf(rule.pattern) >= 0) {
|
||||
|
@ -382,9 +376,9 @@ module.exports = React.createClass({
|
|||
Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, {
|
||||
title: _t('Failed to update keywords'),
|
||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
||||
onFinished: self._refreshFromServer
|
||||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Then, add the new ones
|
||||
Promise.all(removeDeferreds).done(function(resps) {
|
||||
|
@ -398,14 +392,13 @@ module.exports = React.createClass({
|
|||
// Thus, this new rule will join the 'vectorContentRules' set.
|
||||
if (self.state.vectorContentRules.rules.length) {
|
||||
pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(self.state.vectorContentRules.rules[0]);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// ON is default
|
||||
pushRuleVectorStateKind = PushRuleVectorState.ON;
|
||||
pushRuleVectorStateKind = PushRuleVectorState.ON;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i in newKeywords) {
|
||||
for (const i in newKeywords) {
|
||||
const keyword = newKeywords[i];
|
||||
|
||||
if (vectorContentRulesPatterns.indexOf(keyword) < 0) {
|
||||
|
@ -413,13 +406,12 @@ module.exports = React.createClass({
|
|||
deferreds.push(cli.addPushRule
|
||||
('global', 'content', keyword, {
|
||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
||||
pattern: keyword
|
||||
pattern: keyword,
|
||||
}));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
|
||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
||||
pattern: keyword
|
||||
pattern: keyword,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -435,7 +427,7 @@ module.exports = React.createClass({
|
|||
_addDisabledPushRule: function(scope, kind, ruleId, body) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.addPushRule(scope, kind, ruleId, body).then(() =>
|
||||
cli.setPushRuleEnabled(scope, kind, ruleId, false)
|
||||
cli.setPushRuleEnabled(scope, kind, ruleId, false),
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -446,7 +438,7 @@ module.exports = React.createClass({
|
|||
const needsUpdate = [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
for (let kind in rulesets.global) {
|
||||
for (const kind in rulesets.global) {
|
||||
const ruleset = rulesets.global[kind];
|
||||
for (let i = 0; i < ruleset.length; ++i) {
|
||||
const rule = ruleset[i];
|
||||
|
@ -454,9 +446,9 @@ module.exports = React.createClass({
|
|||
console.log("Porting legacy rule", rule);
|
||||
needsUpdate.push( function(kind, rule) {
|
||||
return cli.setPushRuleActions(
|
||||
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions)
|
||||
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions),
|
||||
).then(() =>
|
||||
cli.deletePushRule('global', kind, rule.rule_id)
|
||||
cli.deletePushRule('global', kind, rule.rule_id),
|
||||
).catch( (e) => {
|
||||
console.warn(`Error when porting legacy rule: ${e}`);
|
||||
});
|
||||
|
@ -469,7 +461,7 @@ module.exports = React.createClass({
|
|||
// If some of the rules need to be ported then wait for the porting
|
||||
// to happen and then fetch the rules again.
|
||||
return Promise.all(needsUpdate).then(() =>
|
||||
cli.getPushRules()
|
||||
cli.getPushRules(),
|
||||
);
|
||||
} else {
|
||||
// Otherwise return the rules that we already have.
|
||||
|
@ -480,7 +472,6 @@ module.exports = React.createClass({
|
|||
_refreshFromServer: function() {
|
||||
const self = this;
|
||||
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) {
|
||||
|
||||
/// XXX seriously? wtf is this?
|
||||
MatrixClientPeg.get().pushRules = rulesets;
|
||||
|
||||
|
@ -497,7 +488,7 @@ module.exports = React.createClass({
|
|||
'.m.rule.invite_for_me': 'vector',
|
||||
//'.m.rule.member_event': 'vector',
|
||||
'.m.rule.call': 'vector',
|
||||
'.m.rule.suppress_notices': 'vector'
|
||||
'.m.rule.suppress_notices': 'vector',
|
||||
|
||||
// Others go to others
|
||||
};
|
||||
|
@ -505,7 +496,7 @@ module.exports = React.createClass({
|
|||
// HS default rules
|
||||
const defaultRules = {master: [], vector: {}, others: []};
|
||||
|
||||
for (let kind in rulesets.global) {
|
||||
for (const kind in rulesets.global) {
|
||||
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
|
||||
const r = rulesets.global[kind][i];
|
||||
const cat = rule_categories[r.rule_id];
|
||||
|
@ -514,11 +505,9 @@ module.exports = React.createClass({
|
|||
if (r.rule_id[0] === '.') {
|
||||
if (cat === 'vector') {
|
||||
defaultRules.vector[r.rule_id] = r;
|
||||
}
|
||||
else if (cat === 'master') {
|
||||
} else if (cat === 'master') {
|
||||
defaultRules.master.push(r);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
defaultRules['others'].push(r);
|
||||
}
|
||||
}
|
||||
|
@ -551,9 +540,9 @@ module.exports = React.createClass({
|
|||
'.m.rule.invite_for_me',
|
||||
//'im.vector.rule.member_event',
|
||||
'.m.rule.call',
|
||||
'.m.rule.suppress_notices'
|
||||
'.m.rule.suppress_notices',
|
||||
];
|
||||
for (let i in vectorRuleIds) {
|
||||
for (const i in vectorRuleIds) {
|
||||
const vectorRuleId = vectorRuleIds[i];
|
||||
|
||||
if (vectorRuleId === '_keywords') {
|
||||
|
@ -562,20 +551,19 @@ module.exports = React.createClass({
|
|||
// it corresponds to all content push rules (stored in self.state.vectorContentRule)
|
||||
self.state.vectorPushRules.push({
|
||||
"vectorRuleId": "_keywords",
|
||||
"description" : (
|
||||
"description": (
|
||||
<span>
|
||||
{ _t('Messages containing <span>keywords</span>',
|
||||
{},
|
||||
{ 'span': (sub) =>
|
||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>
|
||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
"vectorState": self.state.vectorContentRules.vectorState
|
||||
"vectorState": self.state.vectorContentRules.vectorState,
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId];
|
||||
const rule = defaultRules.vector[vectorRuleId];
|
||||
|
||||
|
@ -585,7 +573,7 @@ module.exports = React.createClass({
|
|||
|
||||
self.state.vectorPushRules.push({
|
||||
"vectorRuleId": vectorRuleId,
|
||||
"description" : _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
|
||||
"description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
|
||||
"rule": rule,
|
||||
"vectorState": vectorState,
|
||||
});
|
||||
|
@ -604,7 +592,7 @@ module.exports = React.createClass({
|
|||
'.m.rule.fallback': _t('Notify me for anything else'),
|
||||
};
|
||||
|
||||
for (let i in defaultRules.others) {
|
||||
for (const i in defaultRules.others) {
|
||||
const rule = defaultRules.others[i];
|
||||
const ruleDescription = otherRulesDescriptions[rule.rule_id];
|
||||
|
||||
|
@ -622,12 +610,12 @@ module.exports = React.createClass({
|
|||
|
||||
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
|
||||
self.setState({
|
||||
phase: self.phases.DISPLAY
|
||||
phase: self.phases.DISPLAY,
|
||||
});
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
self.setState({
|
||||
phase: self.phases.ERROR
|
||||
phase: self.phases.ERROR,
|
||||
});
|
||||
}).finally(() => {
|
||||
// actually explicitly update our state having been deep-manipulating it
|
||||
|
@ -645,12 +633,12 @@ module.exports = React.createClass({
|
|||
const cli = MatrixClientPeg.get();
|
||||
|
||||
return cli.setPushRuleActions(
|
||||
'global', rule.kind, rule.rule_id, actions
|
||||
'global', rule.kind, rule.rule_id, actions,
|
||||
).then( function() {
|
||||
// Then, if requested, enabled or disabled the rule
|
||||
if (undefined != enabled) {
|
||||
return cli.setPushRuleEnabled(
|
||||
'global', rule.kind, rule.rule_id, enabled
|
||||
'global', rule.kind, rule.rule_id, enabled,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -689,7 +677,7 @@ module.exports = React.createClass({
|
|||
|
||||
renderNotifRulesTableRows: function() {
|
||||
const rows = [];
|
||||
for (let i in this.state.vectorPushRules) {
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
|
||||
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
|
||||
|
@ -769,20 +757,20 @@ module.exports = React.createClass({
|
|||
// This only supports the first email address in your profile for now
|
||||
emailNotificationsRow = this.emailNotificationsRow(
|
||||
emailThreepids[0].address,
|
||||
`${_t('Enable email notifications')} (${emailThreepids[0].address})`
|
||||
`${_t('Enable email notifications')} (${emailThreepids[0].address})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Build external push rules
|
||||
const externalRules = [];
|
||||
for (let i in this.state.externalPushRules) {
|
||||
for (const i in this.state.externalPushRules) {
|
||||
const rule = this.state.externalPushRules[i];
|
||||
externalRules.push(<li>{ _t(rule.description) }</li>);
|
||||
}
|
||||
|
||||
// Show keywords not displayed by the vector UI as a single external push rule
|
||||
let externalKeywords = [];
|
||||
for (let i in this.state.externalContentRules) {
|
||||
for (const i in this.state.externalContentRules) {
|
||||
const rule = this.state.externalContentRules[i];
|
||||
externalKeywords.push(rule.pattern);
|
||||
}
|
||||
|
@ -793,7 +781,7 @@ module.exports = React.createClass({
|
|||
|
||||
let devicesSection;
|
||||
if (this.state.pushers === undefined) {
|
||||
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>
|
||||
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>;
|
||||
} else if (this.state.pushers.length == 0) {
|
||||
devicesSection = null;
|
||||
} else {
|
||||
|
@ -824,7 +812,7 @@ module.exports = React.createClass({
|
|||
advancedSettings = (
|
||||
<div>
|
||||
<h3>{ _t('Advanced notification settings') }</h3>
|
||||
{ _t('There are advanced notifications which are not shown here') }.<br/>
|
||||
{ _t('There are advanced notifications which are not shown here') }.<br />
|
||||
{ _t('You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply') }.
|
||||
<ul>
|
||||
{ externalRules }
|
||||
|
@ -915,5 +903,5 @@ module.exports = React.createClass({
|
|||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -92,7 +92,8 @@ module.exports = React.createClass({
|
|||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
const PersistentApp = sdk.getComponent('elements.PersistentApp');
|
||||
return <PersistentApp />;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -125,14 +125,15 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const VideoView = sdk.getComponent('voip.VideoView');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let voice;
|
||||
if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) {
|
||||
const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
|
||||
voice = (
|
||||
<div className="mx_CallView_voice" onClick={this.props.onClick}>
|
||||
<AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
|
||||
{ _t("Active call (%(roomName)s)", {roomName: callRoom.name}) }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,15 @@ import dis from '../../../dispatcher';
|
|||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function getFullScreenElement() {
|
||||
return (
|
||||
document.fullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.msFullscreenElement
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'VideoView',
|
||||
|
||||
|
@ -88,7 +97,7 @@ module.exports = React.createClass({
|
|||
element.msRequestFullscreen
|
||||
);
|
||||
requestMethod.call(element);
|
||||
} else {
|
||||
} else if (getFullScreenElement()) {
|
||||
const exitMethod = (
|
||||
document.exitFullscreen ||
|
||||
document.mozCancelFullScreen ||
|
||||
|
@ -108,10 +117,7 @@ module.exports = React.createClass({
|
|||
const VideoFeed = sdk.getComponent('voip.VideoFeed');
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const fullscreenElement = (document.fullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.webkitFullscreenElement);
|
||||
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
||||
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxHeight;
|
||||
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
|
||||
{ "mx_VideoView_localVideoFeed_flipped":
|
||||
SettingsStore.getValue('VideoView.flipVideoHorizontally'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue