Merge remote-tracking branch 'upstream/develop' into hs/upload-limits

This commit is contained in:
Will Hunt 2018-10-15 19:20:55 +01:00
commit 5a72a5863f
232 changed files with 13386 additions and 11189 deletions

View file

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

View file

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

View file

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

View file

@ -15,10 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import {EventStatus} from 'matrix-js-sdk';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
@ -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 = (

View file

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

View file

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

View file

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

View file

@ -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 '=_-./'");
}

View file

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

View file

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -139,8 +139,11 @@ module.exports = React.createClass({
</div>
{ editableItems }
{ this.props.canEdit ?
// This is slightly evil; we want a new instance of
// EditableItem when the list grows. To make sure it's
// reset to its initial state.
<EditableItem
key={-1}
key={editableItems.length}
initialValue={this.props.newItem}
onAdd={this.onItemAdded}
onChange={this.onNewItemChanged}

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -28,6 +28,7 @@ import SdkConfig from '../../../SdkConfig';
*/
class PasswordLogin extends React.Component {
static defaultProps = {
onError: function() {},
onUsernameChanged: function() {},
onPasswordChanged: function() {},
onPhoneCountryChanged: function() {},
@ -56,33 +57,64 @@ class PasswordLogin extends React.Component {
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
this.onPasswordChanged = this.onPasswordChanged.bind(this);
this.isLoginEmpty = this.isLoginEmpty.bind(this);
}
componentWillMount() {
this._passwordField = null;
this._loginField = null;
}
componentWillReceiveProps(nextProps) {
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
field_input_incorrect(this._passwordField);
field_input_incorrect(this.isLoginEmpty() ? this._loginField : this._passwordField);
}
}
onSubmitForm(ev) {
ev.preventDefault();
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
this.props.onSubmit(
'', // XXX: Synapse breaks if you send null here:
this.state.phoneCountry,
this.state.phoneNumber,
this.state.password,
);
let username = ''; // XXX: Synapse breaks if you send null here:
let phoneCountry = null;
let phoneNumber = null;
let error;
switch (this.state.loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
username = this.state.username;
if (!username) {
error = _t('The email field must not be blank.');
}
break;
case PasswordLogin.LOGIN_FIELD_MXID:
username = this.state.username;
if (!username) {
error = _t('The user name field must not be blank.');
}
break;
case PasswordLogin.LOGIN_FIELD_PHONE:
phoneCountry = this.state.phoneCountry;
phoneNumber = this.state.phoneNumber;
if (!phoneNumber) {
error = _t('The phone number field must not be blank.');
}
break;
}
if (error) {
this.props.onError(error);
return;
}
if (!this.state.password) {
this.props.onError(_t('The password field must not be blank.'));
return;
}
this.props.onSubmit(
this.state.username,
null,
null,
username,
phoneCountry,
phoneNumber,
this.state.password,
);
}
@ -93,6 +125,7 @@ class PasswordLogin extends React.Component {
}
onLoginTypeChange(loginType) {
this.props.onError(null); // send a null error to clear any error messages
this.setState({
loginType: loginType,
username: "", // Reset because email and username use the same state
@ -126,8 +159,10 @@ class PasswordLogin extends React.Component {
switch (loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
classes.mx_Login_email = true;
classes.error = this.props.loginIncorrect && !this.state.username;
return <input
className={classNames(classes)}
ref={(e) => {this._loginField = e;}}
key="email_input"
type="text"
name="username" // make it a little easier for browser's remember-password
@ -139,8 +174,10 @@ class PasswordLogin extends React.Component {
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
classes.mx_Login_username = true;
classes.error = this.props.loginIncorrect && !this.state.username;
return <input
className={classNames(classes)}
ref={(e) => {this._loginField = e;}}
key="username_input"
type="text"
name="username" // make it a little easier for browser's remember-password
@ -153,14 +190,14 @@ class PasswordLogin extends React.Component {
autoFocus
disabled={disabled}
/>;
case PasswordLogin.LOGIN_FIELD_PHONE:
case PasswordLogin.LOGIN_FIELD_PHONE: {
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
classes.mx_Login_phoneNumberField = true;
classes.mx_Login_field_has_prefix = true;
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
return <div className="mx_Login_phoneSection">
<CountryDropdown
className="mx_Login_phoneCountry mx_Login_field_prefix"
ref="phone_country"
onOptionChange={this.onPhoneCountryChanged}
value={this.state.phoneCountry}
isSmall={true}
@ -169,7 +206,7 @@ class PasswordLogin extends React.Component {
/>
<input
className={classNames(classes)}
ref="phoneNumber"
ref={(e) => {this._loginField = e;}}
key="phone_input"
type="text"
name="phoneNumber"
@ -180,6 +217,17 @@ class PasswordLogin extends React.Component {
disabled={disabled}
/>
</div>;
}
}
}
isLoginEmpty() {
switch (this.state.loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
case PasswordLogin.LOGIN_FIELD_MXID:
return !this.state.username;
case PasswordLogin.LOGIN_FIELD_PHONE:
return !this.state.phoneCountry || !this.state.phoneNumber;
}
}
@ -207,7 +255,7 @@ class PasswordLogin extends React.Component {
const pwFieldClass = classNames({
mx_Login_field: true,
mx_Login_field_disabled: matrixIdText === '',
error: this.props.loginIncorrect,
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
});
const Dropdown = sdk.getComponent('elements.Dropdown');
@ -258,6 +306,7 @@ PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,

View file

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

View file

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

View file

@ -16,6 +16,7 @@ limitations under the License.
'use strict';
import React from 'react';
import MImageBody from './MImageBody';
import sdk from '../../../index';

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

View file

@ -72,14 +72,12 @@ export default React.createClass({
_updateRelatedGroups() {
if (this.unmounted) return;
const relatedGroupsEvent = this.context.matrixClient
.getRoom(this.props.mxEvent.getRoomId())
.currentState
.getStateEvents('m.room.related_groups', '');
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
if (!room) return;
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
this.setState({
relatedGroups: relatedGroupsEvent ?
relatedGroupsEvent.getContent().groups || []
: [],
relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
});
},
@ -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}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Travis Ralston
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,6 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {MatrixClient} from "matrix-js-sdk";
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require("../../../index");
@ -29,6 +31,10 @@ module.exports = React.createClass({
room: PropTypes.object,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
},
saveSettings: function() {
const promises = [];
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
@ -39,42 +45,58 @@ module.exports = React.createClass({
render: function() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const roomId = this.props.room.roomId;
const isEncrypted = this.context.matrixClient.isRoomEncrypted(roomId);
let previewsForAccount = null;
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
previewsForAccount = (
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
);
} else {
previewsForAccount = (
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
);
}
let previewsForRoom = null;
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
previewsForRoom = (
<label>
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM}
roomId={this.props.room.roomId}
isExplicit={true}
manualSave={true}
ref="urlPreviewsRoom" />
</label>
);
} else {
let str = _td("URL previews are enabled by default for participants in this room.");
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
str = _td("URL previews are disabled by default for participants in this room.");
if (!isEncrypted) {
// Only show account setting state and room state setting state in non-e2ee rooms where they apply
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
if (accountEnabled) {
previewsForAccount = (
_t("You have <a>enabled</a> URL previews by default.", {}, {
'a': (sub)=><a href="#/settings">{ sub }</a>,
})
);
} else if (accountEnabled) {
previewsForAccount = (
_t("You have <a>disabled</a> URL previews by default.", {}, {
'a': (sub)=><a href="#/settings">{ sub }</a>,
})
);
}
previewsForRoom = (<label>{ _t(str) }</label>);
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
previewsForRoom = (
<label>
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM}
roomId={roomId}
isExplicit={true}
manualSave={true}
ref="urlPreviewsRoom" />
</label>
);
} else {
let str = _td("URL previews are enabled by default for participants in this room.");
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
str = _td("URL previews are disabled by default for participants in this room.");
}
previewsForRoom = (<label>{ _t(str) }</label>);
}
} else {
previewsForAccount = (
_t("In encrypted rooms, like this one, URL previews are disabled by default to ensure that your " +
"homeserver (where the previews are generated) cannot gather information about links you see in " +
"this room.")
);
}
const previewsForRoomAccount = (
<SettingsFlag name="urlPreviewsEnabled"
const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
level={SettingLevel.ROOM_ACCOUNT}
roomId={this.props.room.roomId}
roomId={roomId}
manualSave={true}
ref="urlPreviewsSelf"
/>
@ -83,8 +105,13 @@ module.exports = React.createClass({
return (
<div className="mx_RoomSettings_toggles">
<h3>{ _t("URL Previews") }</h3>
<label>{ previewsForAccount }</label>
<div>
{ _t('When someone puts a URL in their message, a URL preview can be shown to give more ' +
'information about that link such as the title, description, and an image from the website.') }
</div>
<div>
{ previewsForAccount }
</div>
{ previewsForRoom }
<label>{ previewsForRoomAccount }</label>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
import { _t } from '../../../languageHandler';
import Widgets from '../../../utils/widgets';
import AppTile from '../elements/AppTile';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
@ -24,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}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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