diff --git a/src/CallHandler.js b/src/CallHandler.js index 9118ee1973..015160a1fe 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -273,8 +273,11 @@ function _onAction(payload) { break; case 'incoming_call': if (module.exports.getAnyActiveCall()) { - payload.call.hangup("busy"); - return; // don't allow >1 call to be received, hangup newer one. + // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup. + // we avoid rejecting with "busy" in case the user wants to answer it on a different device. + // in future we could signal a "local busy" as a warning to the caller. + // see https://github.com/vector-im/vector-web/issues/1964 + return; } // if the runtime env doesn't do VoIP, stop here. diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index c0792e6d14..6a8d903df8 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -69,7 +69,7 @@ var sanitizeHtmlParams = { allowedAttributes: { // custom ones first: font: [ 'color' ], // custom to matrix - a: [ 'href', 'name', 'target' ], // remote target: custom to matrix + a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix // We don't currently allow img itself by default, but this // would make sense if we did img: [ 'src' ], @@ -81,7 +81,7 @@ var sanitizeHtmlParams = { allowedSchemesByTag: { img: [ 'data' ], }, - + transformTags: { // custom to matrix // add blank targets to all hyperlinks except vector URLs 'a': function(tagName, attribs) { @@ -92,6 +92,7 @@ var sanitizeHtmlParams = { else { attribs.target = '_blank'; } + attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; }, }, diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js new file mode 100644 index 0000000000..00cad23791 --- /dev/null +++ b/src/RoomNotifs.js @@ -0,0 +1,164 @@ +/* +Copyright 2016 OpenMarket 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 MatrixClientPeg from './MatrixClientPeg'; +import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; +import q from 'q'; + +export const ALL_MESSAGES_LOUD = 'all_messages_loud'; +export const ALL_MESSAGES = 'all_messages'; +export const MENTIONS_ONLY = 'mentions_only'; +export const MUTE = 'mute'; + +export function getRoomNotifsState(roomId) { + if (MatrixClientPeg.get().isGuest()) return RoomNotifs.ALL_MESSAGES; + + // look through the override rules for a rule affecting this room: + // if one exists, it will take precedence. + const muteRule = findOverrideMuteRule(roomId); + if (muteRule) { + return MUTE; + } + + // for everything else, look at the room rule. + const roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId); + + // XXX: We have to assume the default is to notify for all messages + // (in particular this will be 'wrong' for one to one rooms because + // they will notify loudly for all messages) + if (!roomRule || !roomRule.enabled) return ALL_MESSAGES; + + // a mute at the room level will still allow mentions + // to notify + if (isMuteRule(roomRule)) return MENTIONS_ONLY; + + const actionsObject = PushProcessor.actionListToActionsObject(roomRule.actions); + if (actionsObject.tweaks.sound) return ALL_MESSAGES_LOUD; + + return null; +} + +export function setRoomNotifsState(roomId, newState) { + if (newState == MUTE) { + return setRoomNotifsStateMuted(roomId); + } else { + return setRoomNotifsStateUnmuted(roomId, newState); + } +} + +function setRoomNotifsStateMuted(roomId) { + const cli = MatrixClientPeg.get(); + const promises = []; + + // delete the room rule + const roomRule = cli.getRoomPushRule('global', roomId); + if (roomRule) { + promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id)); + } + + // add/replace an override rule to squelch everything in this room + // NB. We use the room ID as the name of this rule too, although this + // is an override rule, not a room rule: it still pertains to this room + // though, so using the room ID as the rule ID is logical and prevents + // duplicate copies of the rule. + promises.push(cli.addPushRule('global', 'override', roomId, { + conditions: [ + { + kind: 'event_match', + key: 'room_id', + pattern: roomId, + } + ], + actions: [ + 'dont_notify', + ] + })); + + return q.all(promises); +} + +function setRoomNotifsStateUnmuted(roomId, newState) { + const cli = MatrixClientPeg.get(); + const promises = []; + + const overrideMuteRule = findOverrideMuteRule(roomId); + if (overrideMuteRule) { + promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id)); + } + + if (newState == 'all_messages') { + const roomRule = cli.getRoomPushRule('global', roomId); + if (roomRule) { + promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id)); + } + } else if (newState == 'mentions_only') { + promises.push(cli.addPushRule('global', 'room', roomId, { + actions: [ + 'dont_notify', + ] + })); + // https://matrix.org/jira/browse/SPEC-400 + promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true)); + } else if ('all_messages_loud') { + promises.push(cli.addPushRule('global', 'room', roomId, { + actions: [ + 'notify', + { + set_tweak: 'sound', + value: 'default', + } + ] + })); + // https://matrix.org/jira/browse/SPEC-400 + promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true)); + } + + return q.all(promises); +} + +function findOverrideMuteRule(roomId) { + for (const rule of MatrixClientPeg.get().pushRules['global'].override) { + if (isRuleForRoom(roomId, rule)) { + if (isMuteRule(rule) && rule.enabled) { + return rule; + } + } + } + return null; +} + +function isRuleForRoom(roomId, rule) { + if (rule.conditions.length !== 1) { + return false; + } + const cond = rule.conditions[0]; + if ( + cond.kind == 'event_match' && + cond.key == 'room_id' && + cond.pattern == roomId + ) { + return true; + } + return false; +} + +function isMuteRule(rule) { + return ( + rule.actions.length == 1 && + rule.actions[0] == 'dont_notify' + ); +} + diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 67607b4996..d145cebfe0 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -34,8 +34,10 @@ class ScalarAuthClient { defer.reject(err); } else if (response.statusCode / 100 !== 2) { defer.reject({statusCode: response.statusCode}); + } else if (!body || !body.scalar_token) { + defer.reject(new Error("Missing scalar_token in response")); } else { - defer.resolve(body.access_token); + defer.resolve(body.scalar_token); } }); diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 4a28103210..2a8c7b383a 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -94,7 +94,7 @@ CommandEntry.fromCommands = function(commandArray) { class MemberEntry extends Entry { constructor(member) { - super(member.name || member.userId); + super((member.name || member.userId).replace(' (IRC)', '')); this.member = member; this.kind = 'member'; } diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 2f416daf95..dbad084024 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -60,7 +60,7 @@ module.exports = React.createClass({ return (
- + Download {text} diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 13f9cf4c19..ec594af2ce 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -134,7 +134,7 @@ module.exports = React.createClass({ onMouseLeave={this.onImageLeave} />
- + Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" }) diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index ba438c1d12..60f4f8abc0 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -123,7 +123,7 @@ module.exports = React.createClass({
{ img }
- +
{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }
{ p["og:description"] } diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 0b37847257..59e186da06 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -67,6 +67,11 @@ module.exports = React.createClass({ componentWillMount: function() { this._cancelDeviceList = null; + // only display the devices list if our client supports E2E *and* the + // feature is enabled in the user settings + this._enableDevices = MatrixClientPeg.get().isCryptoEnabled() && + UserSettingsStore.isFeatureEnabled("e2e_encryption"); + this.setState({ existingOneToOneRoomId: this.getExistingOneToOneRoomId() }); @@ -147,6 +152,10 @@ module.exports = React.createClass({ }, onDeviceVerificationChanged: function(userId, device) { + if (!this._enableDevices) { + return; + } + if (userId == this.props.member.userId) { // no need to re-download the whole thing; just update our copy of // the list. @@ -170,6 +179,10 @@ module.exports = React.createClass({ }, _downloadDeviceList: function(member) { + if (!this._enableDevices) { + return; + } + var cancelled = false; this._cancelDeviceList = function() { cancelled = true; } @@ -532,7 +545,7 @@ module.exports = React.createClass({ }, _renderDevices: function() { - if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) { + if (!this._enableDevices) { return null; } diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index abc90ae486..1896207c09 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -47,16 +47,6 @@ module.exports = React.createClass({ tags[tagName] = ['yep']; }); - var areNotifsMuted = false; - if (!MatrixClientPeg.get().isGuest()) { - var roomPushRule = MatrixClientPeg.get().getRoomPushRule("global", this.props.room.roomId); - if (roomPushRule) { - if (0 <= roomPushRule.actions.indexOf("dont_notify")) { - areNotifsMuted = true; - } - } - } - return { name: this._yankValueFromEvent("m.room.name", "name"), topic: this._yankValueFromEvent("m.room.topic", "topic"), @@ -66,7 +56,6 @@ module.exports = React.createClass({ power_levels_changed: false, tags_changed: false, tags: tags, - areNotifsMuted: areNotifsMuted, // isRoomPublished is loaded async in componentWillMount so when the component // inits, the saved value will always be undefined, however getInitialState() // is also called from the saving code so we must return the correct value here @@ -188,12 +177,6 @@ module.exports = React.createClass({ } - if (this.state.areNotifsMuted !== originalState.areNotifsMuted) { - promises.push(MatrixClientPeg.get().setRoomMutePushRule( - "global", roomId, this.state.areNotifsMuted - )); - } - // power levels var powerLevels = this._getPowerLevels(); if (powerLevels) { @@ -647,12 +630,6 @@ module.exports = React.createClass({ { tagsSection }
-

Who can access this room?

{ inviteGuestWarning } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index ae0ffafae5..dd1ca125aa 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -22,6 +22,7 @@ var dis = require("../../../dispatcher"); var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); var ContextualMenu = require('../../structures/ContextualMenu'); +var RoomNotifs = require('../../../RoomNotifs'); module.exports = React.createClass({ displayName: 'RoomTile', @@ -43,43 +44,41 @@ module.exports = React.createClass({ }, getInitialState: function() { - var areNotifsMuted = false; - var cli = MatrixClientPeg.get(); - if (!cli.isGuest()) { - var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId); - if (roomPushRule) { - if (0 <= roomPushRule.actions.indexOf("dont_notify")) { - areNotifsMuted = true; - } - } - } - return({ hover : false, badgeHover : false, notificationTagMenu: false, roomTagMenu: false, - areNotifsMuted: areNotifsMuted, + notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), }); }, - onAction: function(payload) { - switch (payload.action) { - case 'notification_change': - // Is the notification about this room? - if (payload.roomId === this.props.room.roomId) { - this.setState( { areNotifsMuted : payload.areNotifsMuted }); - } - break; + _shouldShowNotifBadge: function() { + const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD]; + return showBadgeInStates.indexOf(this.state.notifState) > -1; + }, + + _shouldShowMentionBadge: function() { + return this.state.notifState != RoomNotifs.MUTE; + }, + + onAccountData: function(accountDataEvent) { + if (accountDataEvent.getType() == 'm.push_rules') { + this.setState({ + notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), + }); } }, - componentDidMount: function() { - this.dispatcherRef = dis.register(this.onAction); + componentWillMount: function() { + MatrixClientPeg.get().on("accountData", this.onAccountData); }, componentWillUnmount: function() { - dis.unregister(this.dispatcherRef); + var cli = MatrixClientPeg.get(); + if (cli) { + MatrixClientPeg.get().removeListener("accountData", this.onAccountData); + } }, onClick: function() { @@ -179,15 +178,19 @@ module.exports = React.createClass({ var notificationCount = this.props.room.getUnreadNotificationCount(); // var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); + const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(); + const mentionBadges = this.props.highlight && this._shouldShowMentionBadge(); + const badges = notifBadges || mentionBadges; + var classes = classNames({ 'mx_RoomTile': true, 'mx_RoomTile_selected': this.props.selected, 'mx_RoomTile_unread': this.props.unread, - 'mx_RoomTile_unreadNotify': notificationCount > 0 && !this.state.areNotifsMuted, - 'mx_RoomTile_highlight': this.props.highlight, + 'mx_RoomTile_unreadNotify': notifBadges, + 'mx_RoomTile_highlight': mentionBadges, 'mx_RoomTile_invited': (me && me.membership == 'invite'), 'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu, - 'mx_RoomTile_noBadges': !(this.props.highlight || (notificationCount > 0 && !this.state.areNotifsMuted)) + 'mx_RoomTile_noBadges': !badges, }); var avatarClasses = classNames({ @@ -214,7 +217,7 @@ module.exports = React.createClass({ if (this.state.badgeHover || this.state.notificationTagMenu) { badgeContent = "\u00B7\u00B7\u00B7"; - } else if (this.props.highlight || (notificationCount > 0 && !this.state.areNotifsMuted)) { + } else if (badges) { var limitedCount = (notificationCount > 99) ? '99+' : notificationCount; badgeContent = notificationCount ? limitedCount : '!'; } else { @@ -230,7 +233,7 @@ module.exports = React.createClass({ var nameClasses = classNames({ 'mx_RoomTile_name': true, 'mx_RoomTile_invite': this.props.isInvite, - 'mx_RoomTile_badgeShown': this.props.highlight || (notificationCount > 0 && !this.state.areNotifsMuted) || this.state.badgeHover || this.state.notificationTagMenu, + 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.notificationTagMenu, }); if (this.props.selected) { diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index a12ef8eaf5..99b7ee5c33 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -137,6 +137,10 @@ matrixLinkify.options = { } }, + linkAttributes: { + rel: 'noopener', + }, + target: function(href, type) { if (type === 'url') { if (href.match(matrixLinkify.VECTOR_URL_PATTERN)) {