Merge branch 'develop' into bwindels/redesign
This commit is contained in:
commit
91ec96c8d3
102 changed files with 5793 additions and 995 deletions
|
@ -87,7 +87,7 @@ module.exports = React.createClass({
|
|||
if (this.unmounted) return;
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>,
|
||||
);
|
||||
|
|
106
src/components/views/dialogs/RoomUpgradeDialog.js
Normal file
106
src/components/views/dialogs/RoomUpgradeDialog.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'RoomUpgradeDialog',
|
||||
|
||||
propTypes: {
|
||||
room: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._targetVersion = this.props.room.shouldUpgradeToVersion();
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
};
|
||||
},
|
||||
|
||||
_onCancelClick: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_onUpgradeClick: function() {
|
||||
this.setState({busy: true});
|
||||
MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, {
|
||||
title: _t("Failed to upgrade room"),
|
||||
description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({busy: false});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
|
||||
let buttons;
|
||||
if (this.state.busy) {
|
||||
buttons = <Spinner />;
|
||||
} else {
|
||||
buttons = <DialogButtons
|
||||
primaryButton={_t(
|
||||
'Upgrade this room to version %(version)s',
|
||||
{version: this._targetVersion},
|
||||
)}
|
||||
primaryButtonClass="danger"
|
||||
hasCancel={true}
|
||||
onPrimaryButtonClick={this._onUpgradeClick}
|
||||
focus={this.props.focus}
|
||||
onCancel={this._onCancelClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_RoomUpgradeDialog"
|
||||
onFinished={this.onCancelled}
|
||||
title={_t("Upgrade Room Version")}
|
||||
contentId='mx_Dialog_content'
|
||||
onFinished={this.props.onFinished}
|
||||
hasCancel={true}
|
||||
>
|
||||
<p>
|
||||
{_t(
|
||||
"Upgrading this room requires closing down the current " +
|
||||
"instance of the room and creating a new room it its place. " +
|
||||
"To give room members the best possible experience, we will:",
|
||||
)}
|
||||
</p>
|
||||
<ol>
|
||||
<li>{_t("Create a new room with the same name, description and avatar")}</li>
|
||||
<li>{_t("Update any local room aliases to point to the new room")}</li>
|
||||
<li>{_t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room")}</li>
|
||||
<li>{_t("Put a link back to the old room at the start of the new room so people can see old messages")}</li>
|
||||
</ol>
|
||||
{buttons}
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -161,6 +161,8 @@ export default class AppTile extends React.Component {
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -437,6 +439,8 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// Force the widget to be non-persistent
|
||||
ActiveWidgetStore.destroyPersistentWidget();
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
|
||||
formatAppTileName() {
|
||||
|
|
|
@ -187,6 +187,9 @@ const Pill = React.createClass({
|
|||
getContent: () => {
|
||||
return {avatar_url: resp.avatar_url};
|
||||
},
|
||||
getDirectionalContent: function() {
|
||||
return this.getContent();
|
||||
},
|
||||
};
|
||||
this.setState({member});
|
||||
}).catch((err) => {
|
||||
|
|
|
@ -15,15 +15,82 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
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 mx_MatrixToolbar_error";
|
||||
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={toolbarClasses}>
|
||||
<div className={classNames(toolbarClasses)}>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t("This homeserver has hit its Monthly Active User limit. Please contact your service administrator to continue using the service.") }
|
||||
{limitError}
|
||||
{' '}
|
||||
{adminContact}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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() {
|
||||
|
@ -273,12 +275,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 +323,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 +333,7 @@ module.exports = React.createClass({
|
|||
showPrefix={true}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
placeholder={phonePlaceholder}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER,
|
||||
|
|
|
@ -76,7 +76,7 @@ export default class MImageBody extends React.Component {
|
|||
onClientSync(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected && this.state.imgError) {
|
||||
// Load the image again
|
||||
|
|
63
src/components/views/messages/RoomCreate.js
Normal file
63
src/components/views/messages/RoomCreate.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import { makeEventPermalink } from '../../../matrix-to';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomCreate',
|
||||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
_onLinkClicked: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const predecessor = this.props.mxEvent.getContent()['predecessor'];
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: predecessor['event_id'],
|
||||
highlighted: true,
|
||||
room_id: predecessor['room_id'],
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const predecessor = this.props.mxEvent.getContent()['predecessor'];
|
||||
if (predecessor === undefined) {
|
||||
return <div />; // We should never have been instaniated in this case
|
||||
}
|
||||
return <div className="mx_CreateEvent">
|
||||
<img className="mx_CreateEvent_image" src="img/room-continuation.svg" />
|
||||
<div className="mx_CreateEvent_header">
|
||||
{_t("This room is a continuation of another conversation.")}
|
||||
</div>
|
||||
<a className="mx_CreateEvent_link"
|
||||
href={makeEventPermalink(predecessor['room_id'], predecessor['event_id'])}
|
||||
onClick={this._onLinkClicked}
|
||||
>
|
||||
{_t("Click here to see older messages.")}
|
||||
</a>
|
||||
</div>;
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -97,18 +98,19 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// save new canonical alias
|
||||
let oldCanonicalAlias = null;
|
||||
if (this.props.canonicalAliasEvent) {
|
||||
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
|
||||
}
|
||||
if (oldCanonicalAlias !== this.state.canonicalAlias) {
|
||||
|
||||
let newCanonicalAlias = this.state.canonicalAlias;
|
||||
|
||||
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
|
||||
console.log("AliasSettings: Updating canonical alias");
|
||||
promises = [Promise.all(promises).then(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.roomId, "m.room.canonical_alias", {
|
||||
alias: this.state.canonicalAlias,
|
||||
alias: newCanonicalAlias,
|
||||
}, "",
|
||||
),
|
||||
)];
|
||||
|
@ -145,6 +147,7 @@ module.exports = React.createClass({
|
|||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
|
||||
this.state.domainToAliases[localDomain].push(alias);
|
||||
|
@ -161,11 +164,18 @@ module.exports = React.createClass({
|
|||
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.props.canonicalAlias) {
|
||||
this.setState({
|
||||
canonicalAlias: alias
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLocalAliasChanged: function(alias, index) {
|
||||
if (alias === "") return; // hit the delete button to delete please
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain][index] = alias;
|
||||
} else {
|
||||
|
@ -184,10 +194,15 @@ module.exports = React.createClass({
|
|||
// promptly setState anyway, it's just about acceptable. The alternative
|
||||
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||
// that, but why bother when we can cut this corner.
|
||||
this.state.domainToAliases[localDomain].splice(index, 1);
|
||||
const alias = this.state.domainToAliases[localDomain].splice(index, 1);
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
});
|
||||
if (this.props.canonicalAlias === alias) {
|
||||
this.setState({
|
||||
canonicalAlias: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onCanonicalAliasChange: function(event) {
|
||||
|
@ -204,12 +219,14 @@ module.exports = React.createClass({
|
|||
|
||||
let canonical_alias_section;
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
let found = false;
|
||||
canonical_alias_section = (
|
||||
<select onChange={this.onCanonicalAliasChange} defaultValue={this.state.canonicalAlias}>
|
||||
<select onChange={this.onCanonicalAliasChange} value={this.state.canonicalAlias}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(self.state.domainToAliases).map(function(domain, i) {
|
||||
return self.state.domainToAliases[domain].map(function(alias, j) {
|
||||
Object.keys(self.state.domainToAliases).map((domain, i) => {
|
||||
return self.state.domainToAliases[domain].map((alias, j) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
|
@ -218,6 +235,12 @@ module.exports = React.createClass({
|
|||
});
|
||||
})
|
||||
}
|
||||
{
|
||||
found || !this.stateCanonicalAlias ? '' :
|
||||
<option value={ this.state.canonicalAlias } key='arbitrary'>
|
||||
{ this.state.canonicalAlias }
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -90,7 +90,7 @@ module.exports = React.createClass({
|
|||
secondary_color: this.state.secondary_color,
|
||||
}).catch(function(err) {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
@ -57,7 +61,6 @@ const stateEventTileTypes = {
|
|||
'm.room.power_levels': 'messages.TextualEvent',
|
||||
'm.room.pinned_events': 'messages.TextualEvent',
|
||||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
|
@ -483,7 +486,9 @@ 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 tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
|
@ -535,6 +540,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
|
||||
|
@ -745,6 +753,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;
|
||||
}
|
||||
|
|
|
@ -429,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, {
|
||||
|
@ -598,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);
|
||||
|
@ -774,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}
|
||||
|
@ -791,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}
|
||||
/>,
|
||||
);
|
||||
|
|
|
@ -32,10 +32,93 @@ module.exports = React.createClass({
|
|||
displayName: 'MemberList',
|
||||
|
||||
getInitialState: function() {
|
||||
this.memberDict = this.getMemberDict();
|
||||
const members = this.roomMembers();
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
// show an empty list
|
||||
return this._getMembersState([]);
|
||||
} else {
|
||||
return this._getMembersState(this.roomMembers());
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._mounted = true;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
cli.on("Room.myMembership", this.onMyMembership);
|
||||
} else {
|
||||
this._listenForMembersChanges();
|
||||
}
|
||||
cli.on("Room", this.onRoom); // invites & joining after peek
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||
this._showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
}
|
||||
},
|
||||
|
||||
_listenForMembersChanges: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("RoomState.events", this.onRoomStateEvent);
|
||||
// We listen for changes to the lastPresenceTs which is essentially
|
||||
// listening for all presence events (we display most of not all of
|
||||
// the information contained in presence events).
|
||||
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// cli.on("Room.timeline", this.onRoomTimeline);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._mounted = false;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
cli.removeListener("Room.myMembership", this.onMyMembership);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
cli.removeListener("Room", this.onRoom);
|
||||
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateList.cancelPendingCall();
|
||||
},
|
||||
|
||||
/**
|
||||
* If lazy loading is enabled, either:
|
||||
* show a spinner and load the members if the user is joined,
|
||||
* or show the members available so far if the user is invited
|
||||
*/
|
||||
_showMembersAccordingToMembershipWithLL: async function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
const membership = room && room.getMyMembership();
|
||||
if (membership === "join") {
|
||||
this.setState({loading: true});
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
} catch (ex) {/* already logged in RoomView */}
|
||||
if (this._mounted) {
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
this._listenForMembersChanges();
|
||||
}
|
||||
} else if (membership === "invite") {
|
||||
// show the members we've got when invited
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getMembersState: function(members) {
|
||||
// set the state after determining _showPresence to make sure it's
|
||||
// taken into account while rerendering
|
||||
return {
|
||||
loading: false,
|
||||
members: members,
|
||||
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
||||
|
@ -48,70 +131,6 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("RoomState.events", this.onRoomStateEvent);
|
||||
cli.on("Room", this.onRoom); // invites
|
||||
// We listen for changes to the lastPresenceTs which is essentially
|
||||
// listening for all presence events (we display most of not all of
|
||||
// the information contained in presence events).
|
||||
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// cli.on("Room.timeline", this.onRoomTimeline);
|
||||
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||
|
||||
this._showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
cli.removeListener("Room", this.onRoom);
|
||||
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// cli.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateList.cancelPendingCall();
|
||||
},
|
||||
|
||||
/*
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
// ignore anything but real-time updates at the end of the room:
|
||||
// updates from pagination will happen when the paginate completes.
|
||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||
|
||||
// treat any activity from a user as implicit presence to update the
|
||||
// ordering of the list whenever someone says something.
|
||||
// Except right now we're not tiebreaking "active now" users in this way
|
||||
// so don't bother for now.
|
||||
if (ev.getSender()) {
|
||||
// console.log("implicit presence from " + ev.getSender());
|
||||
|
||||
var tile = this.refs[ev.getSender()];
|
||||
if (tile) {
|
||||
// work around a race where you might have a room member object
|
||||
// before the user object exists. XXX: why does this ever happen?
|
||||
var all_members = room.currentState.members;
|
||||
var userId = ev.getSender();
|
||||
if (all_members[userId].user === null) {
|
||||
all_members[userId].user = MatrixClientPeg.get().getUser(userId);
|
||||
}
|
||||
this._updateList(); // reorder the membership list
|
||||
}
|
||||
}
|
||||
},
|
||||
*/
|
||||
|
||||
onUserLastPresenceTs(event, user) {
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
|
@ -130,28 +149,40 @@ module.exports = React.createClass({
|
|||
// We listen for room events because when we accept an invite
|
||||
// we need to wait till the room is fully populated with state
|
||||
// before refreshing the member list else we get a stale list.
|
||||
this._updateList();
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
},
|
||||
|
||||
onMyMembership: function(room, membership, oldMembership) {
|
||||
if (room.roomId === this.props.roomId && membership === "join") {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
onRoomMemberName: function(ev, member) {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
onRoomStateEvent: function(event, state) {
|
||||
if (event.getType() === "m.room.third_party_invite") {
|
||||
if (event.getRoomId() === this.props.roomId &&
|
||||
event.getType() === "m.room.third_party_invite") {
|
||||
this._updateList();
|
||||
}
|
||||
},
|
||||
|
||||
_updateList: new rate_limited_func(function() {
|
||||
// console.log("Updating memberlist");
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
const newState = {
|
||||
loading: false,
|
||||
members: this.roomMembers(),
|
||||
};
|
||||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
|
||||
|
@ -159,50 +190,43 @@ module.exports = React.createClass({
|
|||
this.setState(newState);
|
||||
}, 500),
|
||||
|
||||
getMemberDict: function() {
|
||||
if (!this.props.roomId) return {};
|
||||
getMembersWithUser: function() {
|
||||
if (!this.props.roomId) return [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
if (!room) return {};
|
||||
if (!room) return [];
|
||||
|
||||
const all_members = room.currentState.members;
|
||||
const allMembers = Object.values(room.currentState.members);
|
||||
|
||||
Object.keys(all_members).map(function(userId) {
|
||||
allMembers.forEach(function(member) {
|
||||
// work around a race where you might have a room member object
|
||||
// before the user object exists. This may or may not cause
|
||||
// https://github.com/vector-im/vector-web/issues/186
|
||||
if (all_members[userId].user === null) {
|
||||
all_members[userId].user = MatrixClientPeg.get().getUser(userId);
|
||||
if (member.user === null) {
|
||||
member.user = cli.getUser(member.userId);
|
||||
}
|
||||
|
||||
// XXX: this user may have no lastPresenceTs value!
|
||||
// the right solution here is to fix the race rather than leave it as 0
|
||||
});
|
||||
|
||||
return all_members;
|
||||
return allMembers;
|
||||
},
|
||||
|
||||
roomMembers: function() {
|
||||
const all_members = this.memberDict || {};
|
||||
const all_user_ids = Object.keys(all_members);
|
||||
const ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
|
||||
all_user_ids.sort(this.memberSort);
|
||||
|
||||
const to_display = [];
|
||||
let count = 0;
|
||||
for (let i = 0; i < all_user_ids.length; ++i) {
|
||||
const user_id = all_user_ids[i];
|
||||
const m = all_members[user_id];
|
||||
|
||||
if (m.membership === 'join' || m.membership === 'invite') {
|
||||
if ((ConferenceHandler && !ConferenceHandler.isConferenceUser(user_id)) || !ConferenceHandler) {
|
||||
to_display.push(user_id);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return to_display;
|
||||
const allMembers = this.getMembersWithUser();
|
||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||
return (
|
||||
m.membership === 'join' || m.membership === 'invite'
|
||||
) && (
|
||||
!ConferenceHandler ||
|
||||
(ConferenceHandler && !ConferenceHandler.isConferenceUser(m.userId))
|
||||
);
|
||||
});
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
},
|
||||
|
||||
_createOverflowTileJoined: function(overflowCount, totalCount) {
|
||||
|
@ -249,14 +273,12 @@ module.exports = React.createClass({
|
|||
// returns negative if a comes before b,
|
||||
// returns 0 if a and b are equivalent in ordering
|
||||
// returns positive if a comes after b.
|
||||
memberSort: function(userIdA, userIdB) {
|
||||
memberSort: function(memberA, memberB) {
|
||||
// order by last active, with "active now" first.
|
||||
// ...and then by power
|
||||
// ...and then alphabetically.
|
||||
// We could tiebreak instead by "last recently spoken in this room" if we wanted to.
|
||||
|
||||
const memberA = this.memberDict[userIdA];
|
||||
const memberB = this.memberDict[userIdB];
|
||||
const userA = memberA.user;
|
||||
const userB = memberB.user;
|
||||
|
||||
|
@ -306,9 +328,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_filterMembers: function(members, membership, query) {
|
||||
return members.filter((userId) => {
|
||||
const m = this.memberDict[userId];
|
||||
|
||||
return members.filter((m) => {
|
||||
if (query) {
|
||||
query = query.toLowerCase();
|
||||
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
|
||||
|
@ -350,10 +370,9 @@ module.exports = React.createClass({
|
|||
_makeMemberTiles: function(members, membership) {
|
||||
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
|
||||
const memberList = members.map((userId) => {
|
||||
const m = this.memberDict[userId];
|
||||
const memberList = members.map((m) => {
|
||||
return (
|
||||
<MemberTile key={userId} member={m} ref={userId} showPresence={this._showPresence} />
|
||||
<MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -393,6 +412,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <div className="mx_MemberList"><Spinner /></div>;
|
||||
}
|
||||
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -25,6 +25,7 @@ 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"),
|
||||
|
@ -51,7 +52,9 @@ export default class MessageComposer extends React.Component {
|
|||
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 = {
|
||||
inputState: {
|
||||
|
@ -61,6 +64,7 @@ export default class MessageComposer extends React.Component {
|
|||
},
|
||||
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -70,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();
|
||||
|
@ -88,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;
|
||||
|
@ -96,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;
|
||||
}
|
||||
|
||||
|
@ -207,8 +242,18 @@ export default class MessageComposer extends React.Component {
|
|||
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");
|
||||
|
@ -216,11 +261,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);
|
||||
|
@ -262,8 +309,8 @@ export default class MessageComposer extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -322,6 +369,24 @@ export default class MessageComposer extends React.Component {
|
|||
callButton,
|
||||
videoCallButton,
|
||||
);
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
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">
|
||||
|
|
|
@ -336,7 +336,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -35,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) {
|
||||
|
@ -92,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.
|
||||
|
@ -297,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();
|
||||
},
|
||||
|
@ -342,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;
|
||||
}
|
||||
|
||||
|
@ -446,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
|
||||
|
@ -692,7 +699,7 @@ module.exports = React.createClass({
|
|||
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}
|
||||
|
@ -739,6 +746,18 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['m.server_notice']}
|
||||
label={_t('System Alerts')}
|
||||
tagName="m.lowpriority"
|
||||
editable={false}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={false} />
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
|
|
|
@ -98,15 +98,11 @@ module.exports = React.createClass({
|
|||
</div>);
|
||||
}
|
||||
|
||||
const myMember = this.props.room ? this.props.room.currentState.members[
|
||||
MatrixClientPeg.get().credentials.userId
|
||||
] : null;
|
||||
const kicked = (
|
||||
myMember &&
|
||||
myMember.membership == 'leave' &&
|
||||
myMember.events.member.getSender() != MatrixClientPeg.get().credentials.userId
|
||||
);
|
||||
const banned = myMember && myMember.membership == 'ban';
|
||||
const myMember = this.props.room ?
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId()) :
|
||||
null;
|
||||
const kicked = myMember && myMember.isKicked();
|
||||
const banned = myMember && myMember && myMember.membership == 'ban';
|
||||
|
||||
if (this.props.inviterName) {
|
||||
let emailMatchBlock;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -571,6 +572,11 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onRoomUpgradeClick: function() {
|
||||
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: this.props.room});
|
||||
},
|
||||
|
||||
_onRoomMemberMembership: function() {
|
||||
// Update, since our banned user list may have changed
|
||||
this.forceUpdate();
|
||||
|
@ -793,15 +799,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
let leaveButton = null;
|
||||
const myMember = this.props.room.getMember(myUserId);
|
||||
if (myMember) {
|
||||
if (myMember.membership === "join") {
|
||||
const myMemberShip = this.props.room.getMyMembership();
|
||||
if (myMemberShip) {
|
||||
if (myMemberShip === "join") {
|
||||
leaveButton = (
|
||||
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onLeaveClick}>
|
||||
{ _t('Leave room') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (myMember.membership === "leave") {
|
||||
} else if (myMemberShip === "leave") {
|
||||
leaveButton = (
|
||||
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onForgetClick}>
|
||||
{ _t('Forget room') }
|
||||
|
@ -929,6 +935,13 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
|
||||
let roomUpgradeButton = null;
|
||||
if (this.props.room.shouldUpgradeToVersion() && this.props.room.userMayUpgradeRoom(myUserId)) {
|
||||
roomUpgradeButton = <AccessibleButton className="mx_RoomSettings_upgradeButton danger" onClick={this._onRoomUpgradeClick}>
|
||||
{ _t("Upgrade room to version %(ver)s", {ver: this.props.room.shouldUpgradeToVersion()}) }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings">
|
||||
|
||||
|
@ -1039,7 +1052,9 @@ module.exports = React.createClass({
|
|||
|
||||
<h3>{ _t('Advanced') }</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
{ _t('This room\'s internal ID is') } <code>{ this.props.room.roomId }</code>
|
||||
{ _t('Internal room ID: ') } <code>{ this.props.room.roomId }</code><br />
|
||||
{ _t('Room version number: ') } <code>{ this.props.room.getVersion() }</code><br />
|
||||
{ roomUpgradeButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -243,9 +243,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const me = this.props.room.currentState.members[myUserId];
|
||||
|
||||
const isInvite = this.props.room.getMyMembership() === "invite";
|
||||
const notificationCount = this.state.notificationCount;
|
||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||
|
||||
|
@ -259,7 +257,7 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_unread': this.props.unread,
|
||||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
'mx_RoomTile_invited': (me && me.membership === 'invite'),
|
||||
'mx_RoomTile_invited': isInvite,
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
|
@ -275,6 +273,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
let name = this.state.roomName;
|
||||
if (name == undefined || name == null) name = '';
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
let badgeContent;
|
||||
|
|
57
src/components/views/rooms/RoomUpgradeWarningBar.js
Normal file
57
src/components/views/rooms/RoomUpgradeWarningBar.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomUpgradeWarningBar',
|
||||
|
||||
propTypes: {
|
||||
room: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
onUpgradeClick: function() {
|
||||
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: this.props.room});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_RoomUpgradeWarningBar">
|
||||
<div className="mx_RoomUpgradeWarningBar_header">
|
||||
{_t("There is a known vulnerability affecting this room.")}
|
||||
</div>
|
||||
<div className="mx_RoomUpgradeWarningBar_body">
|
||||
{_t("This room version is vulnerable to malicious modification of room state.")}
|
||||
</div>
|
||||
<p className="mx_RoomUpgradeWarningBar_upgradelink">
|
||||
<AccessibleButton onClick={this.onUpgradeClick}>
|
||||
{_t("Click here to upgrade to the latest room version and ensure room integrity is protected.")}
|
||||
</AccessibleButton>
|
||||
</p>
|
||||
<div className="mx_RoomUpgradeWarningBar_small">
|
||||
{_t("Only room administrators will see this warning")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -26,6 +26,15 @@ import dis from '../../../dispatcher';
|
|||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function getFullScreenElement() {
|
||||
return (
|
||||
document.fullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.msFullscreenElement
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'VideoView',
|
||||
|
||||
|
@ -88,7 +97,7 @@ module.exports = React.createClass({
|
|||
element.msRequestFullscreen
|
||||
);
|
||||
requestMethod.call(element);
|
||||
} else {
|
||||
} else if (getFullScreenElement()) {
|
||||
const exitMethod = (
|
||||
document.exitFullscreen ||
|
||||
document.mozCancelFullScreen ||
|
||||
|
@ -108,10 +117,7 @@ module.exports = React.createClass({
|
|||
const VideoFeed = sdk.getComponent('voip.VideoFeed');
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const fullscreenElement = (document.fullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.webkitFullscreenElement);
|
||||
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
||||
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxHeight;
|
||||
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
|
||||
{ "mx_VideoView_localVideoFeed_flipped":
|
||||
SettingsStore.getValue('VideoView.flipVideoHorizontally'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue