Merge branch 'develop' into wmwragg/chat-message-presentation
						commit
						48a2567b82
					
				|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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 }; | ||||
|         }, | ||||
|     }, | ||||
|  |  | |||
|  | @ -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' | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
|  | @ -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); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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'; | ||||
|     } | ||||
|  |  | |||
|  | @ -60,7 +60,7 @@ module.exports = React.createClass({ | |||
|             return ( | ||||
|                 <span className="mx_MFileBody"> | ||||
|                     <div className="mx_MImageBody_download"> | ||||
|                         <a href={cli.mxcUrlToHttp(content.url)} target="_blank"> | ||||
|                         <a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener"> | ||||
|                             <TintableSvg src="img/download.svg" width="12" height="14"/> | ||||
|                             Download {text} | ||||
|                         </a> | ||||
|  |  | |||
|  | @ -134,7 +134,7 @@ module.exports = React.createClass({ | |||
|                             onMouseLeave={this.onImageLeave} /> | ||||
|                     </a> | ||||
|                     <div className="mx_MImageBody_download"> | ||||
|                         <a href={cli.mxcUrlToHttp(content.url)} target="_blank"> | ||||
|                         <a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener"> | ||||
|                             <TintableSvg src="img/download.svg" width="12" height="14"/> | ||||
|                             Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" }) | ||||
|                         </a> | ||||
|  |  | |||
|  | @ -123,7 +123,7 @@ module.exports = React.createClass({ | |||
|             <div className="mx_LinkPreviewWidget" > | ||||
|                 { img } | ||||
|                 <div className="mx_LinkPreviewWidget_caption"> | ||||
|                     <div className="mx_LinkPreviewWidget_title"><a href={ this.props.link } target="_blank">{ p["og:title"] }</a></div> | ||||
|                     <div className="mx_LinkPreviewWidget_title"><a href={ this.props.link } target="_blank" rel="noopener">{ p["og:title"] }</a></div> | ||||
|                     <div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div> | ||||
|                     <div className="mx_LinkPreviewWidget_description" ref="description"> | ||||
|                         { p["og:description"] } | ||||
|  |  | |||
|  | @ -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; | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 } | ||||
| 
 | ||||
|                 <div className="mx_RoomSettings_toggles"> | ||||
|                     <label> | ||||
|                         <input type="checkbox" disabled={ cli.isGuest() } | ||||
|                                onChange={this._onToggle.bind(this, "areNotifsMuted", true, false)} | ||||
|                                defaultChecked={this.state.areNotifsMuted}/> | ||||
|                         'Mention only' notifications for this room | ||||
|                     </label> | ||||
|                     <div className="mx_RoomSettings_settings"> | ||||
|                         <h3>Who can access this room?</h3> | ||||
|                         { inviteGuestWarning } | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -137,6 +137,10 @@ matrixLinkify.options = { | |||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     linkAttributes: { | ||||
|         rel: 'noopener', | ||||
|     }, | ||||
| 
 | ||||
|     target: function(href, type) { | ||||
|         if (type === 'url') { | ||||
|             if (href.match(matrixLinkify.VECTOR_URL_PATTERN)) { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 wmwragg
						wmwragg