Merge branch 'develop' into t3chguy/ctrl-k_tab
						commit
						6301c04590
					
				|  | @ -392,6 +392,7 @@ limitations under the License. | |||
|     overflow-x: overlay; | ||||
|     overflow-y: visible; | ||||
|     max-height: 30vh; | ||||
|     position: static; | ||||
| } | ||||
| 
 | ||||
| .mx_EventTile_content .markdown-body code { | ||||
|  | @ -406,7 +407,7 @@ limitations under the License. | |||
|     visibility: hidden; | ||||
|     cursor: pointer; | ||||
|     top: 6px; | ||||
|     right: 6px; | ||||
|     right: 36px; | ||||
|     width: 19px; | ||||
|     height: 19px; | ||||
|     background-image: url($copy-button-url); | ||||
|  |  | |||
|  | @ -243,6 +243,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { | |||
|             const blob = new Blob([encryptResult.data]); | ||||
|             return matrixClient.uploadContent(blob, { | ||||
|                 progressHandler: progressHandler, | ||||
|                 includeFilename: false, | ||||
|             }).then(function(url) { | ||||
|                 // If the attachment is encrypted then bundle the URL along
 | ||||
|                 // with the information needed to decrypt the attachment and
 | ||||
|  |  | |||
|  | @ -0,0 +1,169 @@ | |||
| /* | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| class DecryptionFailure { | ||||
|     constructor(failedEventId) { | ||||
|         this.failedEventId = failedEventId; | ||||
|         this.ts = Date.now(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class DecryptionFailureTracker { | ||||
|     // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
 | ||||
|     // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
 | ||||
|     // are added to `failuresToTrack`.
 | ||||
|     failures = []; | ||||
| 
 | ||||
|     // Every TRACK_INTERVAL_MS (so as to spread the number of hits done on Analytics),
 | ||||
|     // one DecryptionFailure of this FIFO is removed and tracked.
 | ||||
|     failuresToTrack = []; | ||||
| 
 | ||||
|     // Event IDs of failures that were tracked previously
 | ||||
|     trackedEventHashMap = { | ||||
|         // [eventId]: true
 | ||||
|     }; | ||||
| 
 | ||||
|     // Set to an interval ID when `start` is called
 | ||||
|     checkInterval = null; | ||||
|     trackInterval = null; | ||||
| 
 | ||||
|     // Spread the load on `Analytics` by sending at most 1 event per
 | ||||
|     // `TRACK_INTERVAL_MS`.
 | ||||
|     static TRACK_INTERVAL_MS = 1000; | ||||
| 
 | ||||
|     // Call `checkFailures` every `CHECK_INTERVAL_MS`.
 | ||||
|     static CHECK_INTERVAL_MS = 5000; | ||||
| 
 | ||||
|     // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before moving
 | ||||
|     // the failure to `failuresToTrack`.
 | ||||
|     static GRACE_PERIOD_MS = 5000; | ||||
| 
 | ||||
|     constructor(fn) { | ||||
|         if (!fn || typeof fn !== 'function') { | ||||
|             throw new Error('DecryptionFailureTracker requires tracking function'); | ||||
|         } | ||||
| 
 | ||||
|         this.trackDecryptionFailure = fn; | ||||
|     } | ||||
| 
 | ||||
|     // loadTrackedEventHashMap() {
 | ||||
|     //     this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {};
 | ||||
|     // }
 | ||||
| 
 | ||||
|     // saveTrackedEventHashMap() {
 | ||||
|     //     localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
 | ||||
|     // }
 | ||||
| 
 | ||||
|     eventDecrypted(e) { | ||||
|         if (e.isDecryptionFailure()) { | ||||
|             this.addDecryptionFailureForEvent(e); | ||||
|         } else { | ||||
|             // Could be an event in the failures, remove it
 | ||||
|             this.removeDecryptionFailuresForEvent(e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     addDecryptionFailureForEvent(e) { | ||||
|         this.failures.push(new DecryptionFailure(e.getId())); | ||||
|     } | ||||
| 
 | ||||
|     removeDecryptionFailuresForEvent(e) { | ||||
|         this.failures = this.failures.filter((f) => f.failedEventId !== e.getId()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Start checking for and tracking failures. | ||||
|      */ | ||||
|     start() { | ||||
|         this.checkInterval = setInterval( | ||||
|             () => this.checkFailures(Date.now()), | ||||
|             DecryptionFailureTracker.CHECK_INTERVAL_MS, | ||||
|         ); | ||||
| 
 | ||||
|         this.trackInterval = setInterval( | ||||
|             () => this.trackFailure(), | ||||
|             DecryptionFailureTracker.TRACK_INTERVAL_MS, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clear state and stop checking for and tracking failures. | ||||
|      */ | ||||
|     stop() { | ||||
|         clearInterval(this.checkInterval); | ||||
|         clearInterval(this.trackInterval); | ||||
| 
 | ||||
|         this.failures = []; | ||||
|         this.failuresToTrack = []; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be | ||||
|      * tracked. Only mark one failure per event ID. | ||||
|      * @param {number} nowTs the timestamp that represents the time now. | ||||
|      */ | ||||
|     checkFailures(nowTs) { | ||||
|         const failuresGivenGrace = []; | ||||
|         const failuresNotReady = []; | ||||
|         while (this.failures.length > 0) { | ||||
|             const f = this.failures.shift(); | ||||
|             if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) { | ||||
|                 failuresGivenGrace.push(f); | ||||
|             } else { | ||||
|                 failuresNotReady.push(f); | ||||
|             } | ||||
|         } | ||||
|         this.failures = failuresNotReady; | ||||
| 
 | ||||
|         // Only track one failure per event
 | ||||
|         const dedupedFailuresMap = failuresGivenGrace.reduce( | ||||
|             (map, failure) => { | ||||
|                 if (!this.trackedEventHashMap[failure.failedEventId]) { | ||||
|                     return map.set(failure.failedEventId, failure); | ||||
|                 } else { | ||||
|                     return map; | ||||
|                 } | ||||
|             }, | ||||
|             // Use a map to preseve key ordering
 | ||||
|             new Map(), | ||||
|         ); | ||||
| 
 | ||||
|         const trackedEventIds = [...dedupedFailuresMap.keys()]; | ||||
| 
 | ||||
|         this.trackedEventHashMap = trackedEventIds.reduce( | ||||
|             (result, eventId) => ({...result, [eventId]: true}), | ||||
|             this.trackedEventHashMap, | ||||
|         ); | ||||
| 
 | ||||
|         // Commented out for now for expediency, we need to consider unbound nature of storing
 | ||||
|         // this in localStorage
 | ||||
|         // this.saveTrackedEventHashMap();
 | ||||
| 
 | ||||
|         const dedupedFailures = dedupedFailuresMap.values(); | ||||
| 
 | ||||
|         this.failuresToTrack = [...this.failuresToTrack, ...dedupedFailures]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * If there is a failure that should be tracked, call the given trackDecryptionFailure | ||||
|      * function with the first failure in the FIFO of failures that should be tracked. | ||||
|      */ | ||||
|     trackFailure() { | ||||
|         if (this.failuresToTrack.length > 0) { | ||||
|             this.trackDecryptionFailure(this.failuresToTrack.shift()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -2,6 +2,7 @@ | |||
| Copyright 2016 Aviral Dasgupta | ||||
| Copyright 2017 Vector Creations Ltd | ||||
| Copyright 2017 New Vector Ltd | ||||
| Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -21,6 +22,7 @@ import { _t, _td } from '../languageHandler'; | |||
| import AutocompleteProvider from './AutocompleteProvider'; | ||||
| import FuzzyMatcher from './FuzzyMatcher'; | ||||
| import {TextualCompletion} from './Components'; | ||||
| import type {SelectionRange} from "./Autocompleter"; | ||||
| 
 | ||||
| // TODO merge this with the factory mechanics of SlashCommands?
 | ||||
| // Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
 | ||||
|  | @ -110,10 +112,9 @@ const COMMANDS = [ | |||
|         args: '', | ||||
|         description: _td('Opens the Developer Tools dialog'), | ||||
|     }, | ||||
|     // Omitting `/markdown` as it only seems to apply to OldComposer
 | ||||
| ]; | ||||
| 
 | ||||
| const COMMAND_RE = /(^\/\w*)/g; | ||||
| const COMMAND_RE = /(^\/\w*)(?: .*)?/g; | ||||
| 
 | ||||
| export default class CommandProvider extends AutocompleteProvider { | ||||
|     constructor() { | ||||
|  | @ -123,23 +124,24 @@ export default class CommandProvider extends AutocompleteProvider { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     async getCompletions(query: string, selection: {start: number, end: number}) { | ||||
|         let completions = []; | ||||
|     async getCompletions(query: string, selection: SelectionRange, force?: boolean) { | ||||
|         const {command, range} = this.getCurrentCommand(query, selection); | ||||
|         if (command) { | ||||
|             completions = this.matcher.match(command[0]).map((result) => { | ||||
|                 return { | ||||
|                     completion: result.command + ' ', | ||||
|                     component: (<TextualCompletion | ||||
|                         title={result.command} | ||||
|                         subtitle={result.args} | ||||
|                         description={_t(result.description)} | ||||
|                         />), | ||||
|                     range, | ||||
|                 }; | ||||
|             }); | ||||
|         } | ||||
|         return completions; | ||||
|         if (!command) return []; | ||||
| 
 | ||||
|         // if the query is just `/` (and the user hit TAB or waits), show them all COMMANDS otherwise FuzzyMatch them
 | ||||
|         const matches = query === '/' ? COMMANDS : this.matcher.match(command[1]); | ||||
|         return matches.map((result) => { | ||||
|             return { | ||||
|                 // If the command is the same as the one they entered, we don't want to discard their arguments
 | ||||
|                 completion: result.command === command[1] ? command[0] : (result.command + ' '), | ||||
|                 component: (<TextualCompletion | ||||
|                     title={result.command} | ||||
|                     subtitle={result.args} | ||||
|                     description={_t(result.description)} | ||||
|                 />), | ||||
|                 range, | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     getName() { | ||||
|  |  | |||
|  | @ -68,8 +68,8 @@ const FilePanel = React.createClass({ | |||
|                     "room": { | ||||
|                         "timeline": { | ||||
|                             "contains_url": true, | ||||
|                             "not_types": [ | ||||
|                                 "m.sticker", | ||||
|                             "types": [ | ||||
|                                 "m.room.message", | ||||
|                             ], | ||||
|                         }, | ||||
|                     }, | ||||
|  |  | |||
|  | @ -1059,7 +1059,7 @@ export default React.createClass({ | |||
|                     <input type="radio" | ||||
|                         value={GROUP_JOINPOLICY_INVITE} | ||||
|                         checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE} | ||||
|                         onClick={this._onJoinableChange} | ||||
|                         onChange={this._onJoinableChange} | ||||
|                     /> | ||||
|                     <div className="mx_GroupView_label_text"> | ||||
|                         { _t('Only people who have been invited') } | ||||
|  | @ -1071,7 +1071,7 @@ export default React.createClass({ | |||
|                     <input type="radio" | ||||
|                         value={GROUP_JOINPOLICY_OPEN} | ||||
|                         checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN} | ||||
|                         onClick={this._onJoinableChange} | ||||
|                         onChange={this._onJoinableChange} | ||||
|                     /> | ||||
|                     <div className="mx_GroupView_label_text"> | ||||
|                         { _t('Everyone') } | ||||
|  | @ -1134,10 +1134,6 @@ export default React.createClass({ | |||
|             let avatarNode; | ||||
|             let nameNode; | ||||
|             let shortDescNode; | ||||
|             const bodyNodes = [ | ||||
|                 this._getMembershipSection(), | ||||
|                 this._getGroupSection(), | ||||
|             ]; | ||||
|             const rightButtons = []; | ||||
|             if (this.state.editing && this.state.isUserPrivileged) { | ||||
|                 let avatarImage; | ||||
|  | @ -1282,7 +1278,8 @@ export default React.createClass({ | |||
|                         </div> | ||||
|                     </div> | ||||
|                     <GeminiScrollbarWrapper className="mx_GroupView_body"> | ||||
|                         { bodyNodes } | ||||
|                         { this._getMembershipSection() } | ||||
|                         { this._getGroupSection() } | ||||
|                     </GeminiScrollbarWrapper> | ||||
|                 </div> | ||||
|             ); | ||||
|  |  | |||
|  | @ -94,6 +94,12 @@ var LeftPanel = React.createClass({ | |||
|             case KeyCode.DOWN: | ||||
|                 this._onMoveFocus(false); | ||||
|                 break; | ||||
|             case KeyCode.ENTER: | ||||
|                 this._onMoveFocus(false); | ||||
|                 if (this.focusedElement) { | ||||
|                     this.focusedElement.click(); | ||||
|                 } | ||||
|                 break; | ||||
|             default: | ||||
|                 handled = false; | ||||
|         } | ||||
|  | @ -105,37 +111,33 @@ var LeftPanel = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     _onMoveFocus: function(up) { | ||||
|         var element = this.focusedElement; | ||||
|         let element = this.focusedElement; | ||||
| 
 | ||||
|         // unclear why this isn't needed
 | ||||
|         // var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
 | ||||
|         // this.focusDirection = up;
 | ||||
| 
 | ||||
|         var descending = false; // are we currently descending or ascending through the DOM tree?
 | ||||
|         var classes; | ||||
|         let descending = false; // are we currently descending or ascending through the DOM tree?
 | ||||
|         let classes; | ||||
| 
 | ||||
|         do { | ||||
|             var child = up ? element.lastElementChild : element.firstElementChild; | ||||
|             var sibling = up ? element.previousElementSibling : element.nextElementSibling; | ||||
|             const child = up ? element.lastElementChild : element.firstElementChild; | ||||
|             const sibling = up ? element.previousElementSibling : element.nextElementSibling; | ||||
| 
 | ||||
|             if (descending) { | ||||
|                 if (child) { | ||||
|                     element = child; | ||||
|                 } | ||||
|                 else if (sibling) { | ||||
|                 } else if (sibling) { | ||||
|                     element = sibling; | ||||
|                 } | ||||
|                 else { | ||||
|                 } else { | ||||
|                     descending = false; | ||||
|                     element = element.parentElement; | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|             } else { | ||||
|                 if (sibling) { | ||||
|                     element = sibling; | ||||
|                     descending = true; | ||||
|                 } | ||||
|                 else { | ||||
|                 } else { | ||||
|                     element = element.parentElement; | ||||
|                 } | ||||
|             } | ||||
|  | @ -147,8 +149,7 @@ var LeftPanel = React.createClass({ | |||
|                     descending = true; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } while(element && !( | ||||
|         } while (element && !( | ||||
|             classes.contains("mx_RoomTile") || | ||||
|             classes.contains("mx_SearchBox_search") || | ||||
|             classes.contains("mx_RoomSubList_ellipsis"))); | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ import PropTypes from 'prop-types'; | |||
| import Matrix from "matrix-js-sdk"; | ||||
| 
 | ||||
| import Analytics from "../../Analytics"; | ||||
| import DecryptionFailureTracker from "../../DecryptionFailureTracker"; | ||||
| import MatrixClientPeg from "../../MatrixClientPeg"; | ||||
| import PlatformPeg from "../../PlatformPeg"; | ||||
| import SdkConfig from "../../SdkConfig"; | ||||
|  | @ -1303,6 +1304,21 @@ export default React.createClass({ | |||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         const dft = new DecryptionFailureTracker((failure) => { | ||||
|             // TODO: Pass reason for failure as third argument to trackEvent
 | ||||
|             Analytics.trackEvent('E2E', 'Decryption failure'); | ||||
|         }); | ||||
| 
 | ||||
|         // Shelved for later date when we have time to think about persisting history of
 | ||||
|         // tracked events across sessions.
 | ||||
|         // dft.loadTrackedEventHashMap();
 | ||||
| 
 | ||||
|         dft.start(); | ||||
| 
 | ||||
|         // When logging out, stop tracking failures and destroy state
 | ||||
|         cli.on("Session.logged_out", () => dft.stop()); | ||||
|         cli.on("Event.decrypted", (e) => dft.eventDecrypted(e)); | ||||
| 
 | ||||
|         const krh = new KeyRequestHandler(cli); | ||||
|         cli.on("crypto.roomKeyRequest", (req) => { | ||||
|             krh.handleKeyRequest(req); | ||||
|  |  | |||
|  | @ -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. | ||||
|  | @ -25,6 +26,9 @@ import sdk from '../../index'; | |||
| 
 | ||||
| import MatrixClientPeg from '../../MatrixClientPeg'; | ||||
| 
 | ||||
| const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
 | ||||
| const continuedTypes = ['m.sticker', 'm.room.message']; | ||||
| 
 | ||||
| /* (almost) stateless UI component which builds the event tiles in the room timeline. | ||||
|  */ | ||||
| module.exports = React.createClass({ | ||||
|  | @ -189,7 +193,7 @@ module.exports = React.createClass({ | |||
|     /** | ||||
|      * Page up/down. | ||||
|      * | ||||
|      * mult: -1 to page up, +1 to page down | ||||
|      * @param {number} mult: -1 to page up, +1 to page down | ||||
|      */ | ||||
|     scrollRelative: function(mult) { | ||||
|         if (this.refs.scrollPanel) { | ||||
|  | @ -199,6 +203,8 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|     /** | ||||
|      * Scroll up/down in response to a scroll key | ||||
|      * | ||||
|      * @param {KeyboardEvent} ev: the keyboard event to handle | ||||
|      */ | ||||
|     handleScrollKey: function(ev) { | ||||
|         if (this.refs.scrollPanel) { | ||||
|  | @ -257,6 +263,7 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|         this.eventNodes = {}; | ||||
| 
 | ||||
|         let visible = false; | ||||
|         let i; | ||||
| 
 | ||||
|         // first figure out which is the last event in the list which we're
 | ||||
|  | @ -297,7 +304,7 @@ module.exports = React.createClass({ | |||
|         // if the readmarker has moved, cancel any active ghost.
 | ||||
|         if (this.currentReadMarkerEventId && this.props.readMarkerEventId && | ||||
|                 this.props.readMarkerVisible && | ||||
|                 this.currentReadMarkerEventId != this.props.readMarkerEventId) { | ||||
|                 this.currentReadMarkerEventId !== this.props.readMarkerEventId) { | ||||
|             this.currentGhostEventId = null; | ||||
|         } | ||||
| 
 | ||||
|  | @ -404,8 +411,8 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|             let isVisibleReadMarker = false; | ||||
| 
 | ||||
|             if (eventId == this.props.readMarkerEventId) { | ||||
|                 var visible = this.props.readMarkerVisible; | ||||
|             if (eventId === this.props.readMarkerEventId) { | ||||
|                 visible = this.props.readMarkerVisible; | ||||
| 
 | ||||
|                 // if the read marker comes at the end of the timeline (except
 | ||||
|                 // for local echoes, which are excluded from RMs, because they
 | ||||
|  | @ -423,11 +430,11 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|             // XXX: there should be no need for a ghost tile - we should just use a
 | ||||
|             // a dispatch (user_activity_end) to start the RM animation.
 | ||||
|             if (eventId == this.currentGhostEventId) { | ||||
|             if (eventId === this.currentGhostEventId) { | ||||
|                 // if we're showing an animation, continue to show it.
 | ||||
|                 ret.push(this._getReadMarkerGhostTile()); | ||||
|             } else if (!isVisibleReadMarker && | ||||
|                        eventId == this.currentReadMarkerEventId) { | ||||
|                        eventId === this.currentReadMarkerEventId) { | ||||
|                 // there is currently a read-up-to marker at this point, but no
 | ||||
|                 // more. Show an animation of it disappearing.
 | ||||
|                 ret.push(this._getReadMarkerGhostTile()); | ||||
|  | @ -449,16 +456,17 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|         // Some events should appear as continuations from previous events of
 | ||||
|         // different types.
 | ||||
|         const continuedTypes = ['m.sticker', 'm.room.message']; | ||||
| 
 | ||||
|         const eventTypeContinues = | ||||
|             prevEvent !== null && | ||||
|             continuedTypes.includes(mxEv.getType()) && | ||||
|             continuedTypes.includes(prevEvent.getType()); | ||||
| 
 | ||||
|         if (prevEvent !== null | ||||
|                 && prevEvent.sender && mxEv.sender | ||||
|                 && mxEv.sender.userId === prevEvent.sender.userId | ||||
|                 && (mxEv.getType() == prevEvent.getType() || eventTypeContinues)) { | ||||
|         // if there is a previous event and it has the same sender as this event
 | ||||
|         // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
 | ||||
|         if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && | ||||
|             (mxEv.getType() === prevEvent.getType() || eventTypeContinues) && | ||||
|             (mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) { | ||||
|             continuation = true; | ||||
|         } | ||||
| 
 | ||||
|  | @ -493,7 +501,7 @@ module.exports = React.createClass({ | |||
|         } | ||||
| 
 | ||||
|         const eventId = mxEv.getId(); | ||||
|         const highlight = (eventId == this.props.highlightedEventId); | ||||
|         const highlight = (eventId === this.props.highlightedEventId); | ||||
| 
 | ||||
|         // we can't use local echoes as scroll tokens, because their event IDs change.
 | ||||
|         // Local echos have a send "status".
 | ||||
|  | @ -632,7 +640,8 @@ module.exports = React.createClass({ | |||
|     render: function() { | ||||
|         const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); | ||||
|         const Spinner = sdk.getComponent("elements.Spinner"); | ||||
|         let topSpinner, bottomSpinner; | ||||
|         let topSpinner; | ||||
|         let bottomSpinner; | ||||
|         if (this.props.backPaginating) { | ||||
|             topSpinner = <li key="_topSpinner"><Spinner /></li>; | ||||
|         } | ||||
|  |  | |||
|  | @ -70,7 +70,7 @@ export default withMatrixClient(React.createClass({ | |||
|         if (this.state.groups) { | ||||
|             const groupNodes = []; | ||||
|             this.state.groups.forEach((g) => { | ||||
|                 groupNodes.push(<GroupTile groupId={g} />); | ||||
|                 groupNodes.push(<GroupTile key={g} groupId={g} />); | ||||
|             }); | ||||
|             contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />; | ||||
|             content = groupNodes.length > 0 ? | ||||
|  | @ -124,7 +124,7 @@ export default withMatrixClient(React.createClass({ | |||
|                         ) } | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div className="mx_MyGroups_joinBox mx_MyGroups_headerCard"> | ||||
|                 {/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard"> | ||||
|                     <AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}> | ||||
|                         <TintableSvg src="img/icons-create-room.svg" width="50" height="50" /> | ||||
|                     </AccessibleButton> | ||||
|  | @ -140,7 +140,7 @@ export default withMatrixClient(React.createClass({ | |||
|                             { 'i': (sub) => <i>{ sub }</i> }) | ||||
|                         } | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 </div>*/} | ||||
|             </div> | ||||
|             <div className="mx_MyGroups_content"> | ||||
|                 { contentHeader } | ||||
|  |  | |||
|  | @ -170,7 +170,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { | |||
|                     { profile } | ||||
|                 </div> | ||||
|                 <DialogButtons primaryButton={_t('Start Chatting')} | ||||
|                     onPrimaryButtonClick={this.props.onNewDMClick} focus="true" /> | ||||
|                     onPrimaryButtonClick={this.props.onNewDMClick} focus={true} /> | ||||
|             </div>; | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ function getOrCreateContainer() { | |||
| } | ||||
| 
 | ||||
| // Greater than that of the ContextualMenu
 | ||||
| const PE_Z_INDEX = 3000; | ||||
| const PE_Z_INDEX = 5000; | ||||
| 
 | ||||
| /* | ||||
|  * Class of component that renders its children in a separate ReactDOM virtual tree | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| /* | ||||
| Copyright 2017, 2018 New Vector Ltd | ||||
| Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -20,8 +21,9 @@ import { MatrixClient } from 'matrix-js-sdk'; | |||
| import sdk from '../../../index'; | ||||
| import dis from '../../../dispatcher'; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| import * as ContextualMenu from "../../structures/ContextualMenu"; | ||||
| import classNames from 'classnames'; | ||||
| import MatrixClientPeg from "../../../MatrixClientPeg"; | ||||
| import {createMenu} from "../../structures/ContextualMenu"; | ||||
| 
 | ||||
| export default React.createClass({ | ||||
|     displayName: 'GroupInviteTile', | ||||
|  | @ -66,29 +68,11 @@ export default React.createClass({ | |||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onBadgeClicked: function(e) { | ||||
|         // Prevent the RoomTile onClick event firing as well
 | ||||
|         e.stopPropagation(); | ||||
|     _showContextMenu: function(x, y, chevronOffset) { | ||||
|         const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); | ||||
| 
 | ||||
|         // Only allow none guests to access the context menu
 | ||||
|         if (this.context.matrixClient.isGuest()) return; | ||||
| 
 | ||||
|         // If the badge is clicked, then no longer show tooltip
 | ||||
|         if (this.props.collapsed) { | ||||
|             this.setState({ hover: false }); | ||||
|         } | ||||
| 
 | ||||
|         const RoomTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); | ||||
|         const elementRect = e.target.getBoundingClientRect(); | ||||
| 
 | ||||
|         // The window X and Y offsets are to adjust position when zoomed in to page
 | ||||
|         const x = elementRect.right + window.pageXOffset + 3; | ||||
|         const chevronOffset = 12; | ||||
|         let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); | ||||
|         y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
 | ||||
| 
 | ||||
|         ContextualMenu.createMenu(RoomTileContextMenu, { | ||||
|             chevronOffset: chevronOffset, | ||||
|         createMenu(GroupInviteTileContextMenu, { | ||||
|             chevronOffset, | ||||
|             left: x, | ||||
|             top: y, | ||||
|             group: this.props.group, | ||||
|  | @ -99,6 +83,38 @@ export default React.createClass({ | |||
|         this.setState({ menuDisplayed: true }); | ||||
|     }, | ||||
| 
 | ||||
|     onContextMenu: function(e) { | ||||
|         // Prevent the RoomTile onClick event firing as well
 | ||||
|         e.preventDefault(); | ||||
|         // Only allow non-guests to access the context menu
 | ||||
|         if (MatrixClientPeg.get().isGuest()) return; | ||||
| 
 | ||||
|         const chevronOffset = 12; | ||||
|         this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset); | ||||
|     }, | ||||
| 
 | ||||
|     onBadgeClicked: function(e) { | ||||
|         // Prevent the RoomTile onClick event firing as well
 | ||||
|         e.stopPropagation(); | ||||
|         // Only allow non-guests to access the context menu
 | ||||
|         if (MatrixClientPeg.get().isGuest()) return; | ||||
| 
 | ||||
|         // If the badge is clicked, then no longer show tooltip
 | ||||
|         if (this.props.collapsed) { | ||||
|             this.setState({ hover: false }); | ||||
|         } | ||||
| 
 | ||||
|         const elementRect = e.target.getBoundingClientRect(); | ||||
| 
 | ||||
|         // The window X and Y offsets are to adjust position when zoomed in to page
 | ||||
|         const x = elementRect.right + window.pageXOffset + 3; | ||||
|         const chevronOffset = 12; | ||||
|         let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); | ||||
|         y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
 | ||||
| 
 | ||||
|         this._showContextMenu(x, y, chevronOffset); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); | ||||
|         const EmojiText = sdk.getComponent('elements.EmojiText'); | ||||
|  | @ -139,7 +155,12 @@ export default React.createClass({ | |||
|         }); | ||||
| 
 | ||||
|         return ( | ||||
|             <AccessibleButton className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> | ||||
|             <AccessibleButton className={classes} | ||||
|                               onClick={this.onClick} | ||||
|                               onMouseEnter={this.onMouseEnter} | ||||
|                               onMouseLeave={this.onMouseLeave} | ||||
|                               onContextMenu={this.onContextMenu} | ||||
|             > | ||||
|                 <div className="mx_RoomTile_avatar"> | ||||
|                     { av } | ||||
|                 </div> | ||||
|  |  | |||
|  | @ -69,7 +69,7 @@ export default React.createClass({ | |||
|     render() { | ||||
|         const GroupTile = sdk.getComponent('groups.GroupTile'); | ||||
|         const input = <input type="checkbox" | ||||
|             onClick={this._onPublicityToggle} | ||||
|             onChange={this._onPublicityToggle} | ||||
|             checked={this.state.isGroupPublicised} | ||||
|         />; | ||||
|         const labelText = !this.state.ready ? _t("Loading...") : | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ import sdk from '../../../index'; | |||
| import dis from '../../../dispatcher'; | ||||
| import FlairStore from '../../../stores/FlairStore'; | ||||
| 
 | ||||
| function nop() {} | ||||
| 
 | ||||
| const GroupTile = React.createClass({ | ||||
|     displayName: 'GroupTile', | ||||
|  | @ -81,7 +82,7 @@ const GroupTile = React.createClass({ | |||
|         ) : null; | ||||
|         // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
 | ||||
|         // instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
 | ||||
|         return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown}> | ||||
|         return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}> | ||||
|             <Droppable droppableId="my-groups-droppable" type="draggable-TagTile"> | ||||
|                 { (droppableProvided, droppableSnapshot) => ( | ||||
|                     <div ref={droppableProvided.innerRef}> | ||||
|  |  | |||
|  | @ -327,6 +327,7 @@ module.exports = React.createClass({ | |||
|                     // will have the correct name when the user tries to download it.
 | ||||
|                     // We can't provide a Content-Disposition header like we would for HTTP.
 | ||||
|                     download: fileName, | ||||
|                     rel: "noopener", | ||||
|                     target: "_blank", | ||||
|                     textContent: _t("Download %(text)s", { text: text }), | ||||
|                 }, "*"); | ||||
|  |  | |||
|  | @ -36,6 +36,7 @@ import * as ContextualMenu from '../../structures/ContextualMenu'; | |||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; | ||||
| import ReplyThread from "../elements/ReplyThread"; | ||||
| import {host as matrixtoHost} from '../../../matrix-to'; | ||||
| 
 | ||||
| linkifyMatrix(linkify); | ||||
| 
 | ||||
|  | @ -304,7 +305,7 @@ module.exports = React.createClass({ | |||
|             // never preview matrix.to links (if anything we should give a smart
 | ||||
|             // preview of the room/user they point to: nobody needs to be reminded
 | ||||
|             // what the matrix.to site looks like).
 | ||||
|             if (host == 'matrix.to') return false; | ||||
|             if (host === matrixtoHost) return false; | ||||
| 
 | ||||
|             if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) { | ||||
|                 // it's a "foo.pl" style link
 | ||||
|  |  | |||
|  | @ -45,8 +45,7 @@ import Markdown from '../../../Markdown'; | |||
| import ComposerHistoryManager from '../../../ComposerHistoryManager'; | ||||
| import MessageComposerStore from '../../../stores/MessageComposerStore'; | ||||
| 
 | ||||
| import {MATRIXTO_URL_PATTERN, MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix'; | ||||
| const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); | ||||
| import {MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix'; | ||||
| const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g'); | ||||
| 
 | ||||
| import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione'; | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| /* | ||||
| Copyright 2015, 2016 OpenMarket Ltd | ||||
| Copyright 2017 New Vector Ltd | ||||
| Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -15,19 +16,17 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const ReactDOM = require("react-dom"); | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| const classNames = require('classnames'); | ||||
| import classNames from 'classnames'; | ||||
| import dis from '../../../dispatcher'; | ||||
| const MatrixClientPeg = require('../../../MatrixClientPeg'); | ||||
| import MatrixClientPeg from '../../../MatrixClientPeg'; | ||||
| import DMRoomMap from '../../../utils/DMRoomMap'; | ||||
| const sdk = require('../../../index'); | ||||
| const ContextualMenu = require('../../structures/ContextualMenu'); | ||||
| const RoomNotifs = require('../../../RoomNotifs'); | ||||
| const FormattingUtils = require('../../../utils/FormattingUtils'); | ||||
| import sdk from '../../../index'; | ||||
| import {createMenu} from '../../structures/ContextualMenu'; | ||||
| import * as RoomNotifs from '../../../RoomNotifs'; | ||||
| import * as FormattingUtils from '../../../utils/FormattingUtils'; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| import ActiveRoomObserver from '../../../ActiveRoomObserver'; | ||||
| import RoomViewStore from '../../../stores/RoomViewStore'; | ||||
|  | @ -72,16 +71,12 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     _shouldShowMentionBadge: function() { | ||||
|         return this.state.notifState != RoomNotifs.MUTE; | ||||
|         return this.state.notifState !== RoomNotifs.MUTE; | ||||
|     }, | ||||
| 
 | ||||
|     _isDirectMessageRoom: function(roomId) { | ||||
|         const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId); | ||||
|         if (dmRooms) { | ||||
|             return true; | ||||
|         } else { | ||||
|             return false; | ||||
|         } | ||||
|         return Boolean(dmRooms); | ||||
|     }, | ||||
| 
 | ||||
|     onRoomTimeline: function(ev, room) { | ||||
|  | @ -99,7 +94,7 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     onAccountData: function(accountDataEvent) { | ||||
|         if (accountDataEvent.getType() == 'm.push_rules') { | ||||
|         if (accountDataEvent.getType() === 'm.push_rules') { | ||||
|             this.setState({ | ||||
|                 notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), | ||||
|             }); | ||||
|  | @ -187,6 +182,32 @@ module.exports = React.createClass({ | |||
|         this.badgeOnMouseLeave(); | ||||
|     }, | ||||
| 
 | ||||
|     _showContextMenu: function(x, y, chevronOffset) { | ||||
|         const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); | ||||
| 
 | ||||
|         createMenu(RoomTileContextMenu, { | ||||
|             chevronOffset, | ||||
|             left: x, | ||||
|             top: y, | ||||
|             room: this.props.room, | ||||
|             onFinished: () => { | ||||
|                 this.setState({ menuDisplayed: false }); | ||||
|                 this.props.refreshSubList(); | ||||
|             }, | ||||
|         }); | ||||
|         this.setState({ menuDisplayed: true }); | ||||
|     }, | ||||
| 
 | ||||
|     onContextMenu: function(e) { | ||||
|         // Prevent the RoomTile onClick event firing as well
 | ||||
|         e.preventDefault(); | ||||
|         // Only allow non-guests to access the context menu
 | ||||
|         if (MatrixClientPeg.get().isGuest()) return; | ||||
| 
 | ||||
|         const chevronOffset = 12; | ||||
|         this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset); | ||||
|     }, | ||||
| 
 | ||||
|     badgeOnMouseEnter: function() { | ||||
|         // Only allow non-guests to access the context menu
 | ||||
|         // and only change it if it needs to change
 | ||||
|  | @ -200,37 +221,25 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     onBadgeClicked: function(e) { | ||||
|         // Only allow none guests to access the context menu
 | ||||
|         if (!MatrixClientPeg.get().isGuest()) { | ||||
|             // If the badge is clicked, then no longer show tooltip
 | ||||
|             if (this.props.collapsed) { | ||||
|                 this.setState({ hover: false }); | ||||
|             } | ||||
| 
 | ||||
|             const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); | ||||
|             const elementRect = e.target.getBoundingClientRect(); | ||||
| 
 | ||||
|             // The window X and Y offsets are to adjust position when zoomed in to page
 | ||||
|             const x = elementRect.right + window.pageXOffset + 3; | ||||
|             const chevronOffset = 12; | ||||
|             let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); | ||||
|             y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
 | ||||
| 
 | ||||
|             const self = this; | ||||
|             ContextualMenu.createMenu(RoomTileContextMenu, { | ||||
|                 chevronOffset: chevronOffset, | ||||
|                 left: x, | ||||
|                 top: y, | ||||
|                 room: this.props.room, | ||||
|                 onFinished: function() { | ||||
|                     self.setState({ menuDisplayed: false }); | ||||
|                     self.props.refreshSubList(); | ||||
|                 }, | ||||
|             }); | ||||
|             this.setState({ menuDisplayed: true }); | ||||
|         } | ||||
|         // Prevent the RoomTile onClick event firing as well
 | ||||
|         e.stopPropagation(); | ||||
|         // Only allow non-guests to access the context menu
 | ||||
|         if (MatrixClientPeg.get().isGuest()) return; | ||||
| 
 | ||||
|         // If the badge is clicked, then no longer show tooltip
 | ||||
|         if (this.props.collapsed) { | ||||
|             this.setState({ hover: false }); | ||||
|         } | ||||
| 
 | ||||
|         const elementRect = e.target.getBoundingClientRect(); | ||||
| 
 | ||||
|         // The window X and Y offsets are to adjust position when zoomed in to page
 | ||||
|         const x = elementRect.right + window.pageXOffset + 3; | ||||
|         const chevronOffset = 12; | ||||
|         let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); | ||||
|         y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
 | ||||
| 
 | ||||
|         this._showContextMenu(x, y, chevronOffset); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|  | @ -250,7 +259,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': (me && me.membership === 'invite'), | ||||
|             'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, | ||||
|             'mx_RoomTile_noBadges': !badges, | ||||
|             'mx_RoomTile_transparent': this.props.transparent, | ||||
|  | @ -268,7 +277,6 @@ module.exports = React.createClass({ | |||
|         let name = this.state.roomName; | ||||
|         name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
 | ||||
| 
 | ||||
|         let badge; | ||||
|         let badgeContent; | ||||
| 
 | ||||
|         if (this.state.badgeHover || this.state.menuDisplayed) { | ||||
|  | @ -280,7 +288,7 @@ module.exports = React.createClass({ | |||
|             badgeContent = '\u200B'; | ||||
|         } | ||||
| 
 | ||||
|         badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>; | ||||
|         const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>; | ||||
| 
 | ||||
|         const EmojiText = sdk.getComponent('elements.EmojiText'); | ||||
|         let label; | ||||
|  | @ -312,16 +320,22 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|         const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); | ||||
| 
 | ||||
|         let directMessageIndicator; | ||||
|         let dmIndicator; | ||||
|         if (this._isDirectMessageRoom(this.props.room.roomId)) { | ||||
|             directMessageIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />; | ||||
|             dmIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />; | ||||
|         } | ||||
| 
 | ||||
|         return <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> | ||||
|         return <AccessibleButton tabIndex="0" | ||||
|                                  className={classes} | ||||
|                                  onClick={this.onClick} | ||||
|                                  onMouseEnter={this.onMouseEnter} | ||||
|                                  onMouseLeave={this.onMouseLeave} | ||||
|                                  onContextMenu={this.onContextMenu} | ||||
|         > | ||||
|             <div className={avatarClasses}> | ||||
|                 <div className="mx_RoomTile_avatar_container"> | ||||
|                     <RoomAvatar room={this.props.room} width={24} height={24} /> | ||||
|                     { directMessageIndicator } | ||||
|                     { dmIndicator } | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div className="mx_RoomTile_nameContainer"> | ||||
|  |  | |||
|  | @ -169,11 +169,18 @@ matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" | |||
|     + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/" | ||||
|     + ")(#.*)"; | ||||
| 
 | ||||
| matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; | ||||
| matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/(([#@!+]).*)"; | ||||
| matrixLinkify.MATRIXTO_MD_LINK_PATTERN = | ||||
|     '\\[([^\\]]*)\\]\\((?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!)[^\\)]*)\\)'; | ||||
|     '\\[([^\\]]*)\\]\\((?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)'; | ||||
| matrixLinkify.MATRIXTO_BASE_URL= baseUrl; | ||||
| 
 | ||||
| const matrixToEntityMap = { | ||||
|     '@': '#/user/', | ||||
|     '#': '#/room/', | ||||
|     '!': '#/room/', | ||||
|     '+': '#/group/', | ||||
| }; | ||||
| 
 | ||||
| matrixLinkify.options = { | ||||
|     events: function(href, type) { | ||||
|         switch (type) { | ||||
|  | @ -204,24 +211,20 @@ matrixLinkify.options = { | |||
|             case 'userid': | ||||
|             case 'groupid': | ||||
|                 return matrixLinkify.MATRIXTO_BASE_URL + '/#/' + href; | ||||
|             default: | ||||
|                 var m; | ||||
|             default: { | ||||
|                 // FIXME: horrible duplication with HtmlUtils' transform tags
 | ||||
|                 m = href.match(matrixLinkify.VECTOR_URL_PATTERN); | ||||
|                 let m = href.match(matrixLinkify.VECTOR_URL_PATTERN); | ||||
|                 if (m) { | ||||
|                     return m[1]; | ||||
|                 } | ||||
|                 m = href.match(matrixLinkify.MATRIXTO_URL_PATTERN); | ||||
|                 if (m) { | ||||
|                     const entity = m[1]; | ||||
|                     if (entity[0] === '@') { | ||||
|                         return '#/user/' + entity; | ||||
|                     } else if (entity[0] === '#' || entity[0] === '!') { | ||||
|                         return '#/room/' + entity; | ||||
|                     } | ||||
|                     if (matrixToEntityMap[entity[0]]) return matrixToEntityMap[entity[0]] + entity; | ||||
|                 } | ||||
| 
 | ||||
|                 return href; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,7 +14,8 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| export const baseUrl = "https://matrix.to"; | ||||
| export const host = "matrix.to"; | ||||
| export const baseUrl = `https://${host}`; | ||||
| 
 | ||||
| export function makeEventPermalink(roomId, eventId) { | ||||
|     return `${baseUrl}/#/${roomId}/${eventId}`; | ||||
|  |  | |||
|  | @ -0,0 +1,185 @@ | |||
| /* | ||||
| 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 expect from 'expect'; | ||||
| 
 | ||||
| import DecryptionFailureTracker from '../src/DecryptionFailureTracker'; | ||||
| 
 | ||||
| import { MatrixEvent } from 'matrix-js-sdk'; | ||||
| 
 | ||||
| function createFailedDecryptionEvent() { | ||||
|     const event = new MatrixEvent({ | ||||
|         event_id: "event-id-" + Math.random().toString(16).slice(2), | ||||
|     }); | ||||
|     event._setClearData( | ||||
|         event._badEncryptedMessage(":("), | ||||
|     ); | ||||
|     return event; | ||||
| } | ||||
| 
 | ||||
| describe('DecryptionFailureTracker', function() { | ||||
|     it('tracks a failed decryption', function(done) { | ||||
|         const failedDecryptionEvent = createFailedDecryptionEvent(); | ||||
|         let trackedFailure = null; | ||||
|         const tracker = new DecryptionFailureTracker((failure) => { | ||||
|             trackedFailure = failure; | ||||
|         }); | ||||
| 
 | ||||
|         tracker.eventDecrypted(failedDecryptionEvent); | ||||
| 
 | ||||
|         // Pretend "now" is Infinity
 | ||||
|         tracker.checkFailures(Infinity); | ||||
| 
 | ||||
|         // Immediately track the newest failure, if there is one
 | ||||
|         tracker.trackFailure(); | ||||
| 
 | ||||
|         expect(trackedFailure).toNotBe(null, 'should track a failure for an event that failed decryption'); | ||||
| 
 | ||||
|         done(); | ||||
|     }); | ||||
| 
 | ||||
|     it('does not track a failed decryption where the event is subsequently successfully decrypted', (done) => { | ||||
|         const decryptedEvent = createFailedDecryptionEvent(); | ||||
|         const tracker = new DecryptionFailureTracker((failure) => { | ||||
|             expect(true).toBe(false, 'should not track an event that has since been decrypted correctly'); | ||||
|         }); | ||||
| 
 | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
| 
 | ||||
|         // Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted
 | ||||
|         decryptedEvent._setClearData({}); | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
| 
 | ||||
|         // Pretend "now" is Infinity
 | ||||
|         tracker.checkFailures(Infinity); | ||||
| 
 | ||||
|         // Immediately track the newest failure, if there is one
 | ||||
|         tracker.trackFailure(); | ||||
|         done(); | ||||
|     }); | ||||
| 
 | ||||
|     it('only tracks a single failure per event, despite multiple failed decryptions for multiple events', (done) => { | ||||
|         const decryptedEvent = createFailedDecryptionEvent(); | ||||
|         const decryptedEvent2 = createFailedDecryptionEvent(); | ||||
| 
 | ||||
|         let count = 0; | ||||
|         const tracker = new DecryptionFailureTracker((failure) => count++); | ||||
| 
 | ||||
|         // Arbitrary number of failed decryptions for both events
 | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
|         tracker.eventDecrypted(decryptedEvent2); | ||||
|         tracker.eventDecrypted(decryptedEvent2); | ||||
|         tracker.eventDecrypted(decryptedEvent2); | ||||
| 
 | ||||
|         // Pretend "now" is Infinity
 | ||||
|         tracker.checkFailures(Infinity); | ||||
| 
 | ||||
|         // Simulated polling of `trackFailure`, an arbitrary number ( > 2 ) times
 | ||||
|         tracker.trackFailure(); | ||||
|         tracker.trackFailure(); | ||||
|         tracker.trackFailure(); | ||||
|         tracker.trackFailure(); | ||||
| 
 | ||||
|         expect(count).toBe(2, count + ' failures tracked, should only track a single failure per event'); | ||||
| 
 | ||||
|         done(); | ||||
|     }); | ||||
| 
 | ||||
|     it('track failures in the order they occured', (done) => { | ||||
|         const decryptedEvent = createFailedDecryptionEvent(); | ||||
|         const decryptedEvent2 = createFailedDecryptionEvent(); | ||||
| 
 | ||||
|         const failures = []; | ||||
|         const tracker = new DecryptionFailureTracker((failure) => failures.push(failure)); | ||||
| 
 | ||||
|         // Indicate decryption
 | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
|         tracker.eventDecrypted(decryptedEvent2); | ||||
| 
 | ||||
|         // Pretend "now" is Infinity
 | ||||
|         tracker.checkFailures(Infinity); | ||||
| 
 | ||||
|         // Simulated polling of `trackFailure`, an arbitrary number ( > 2 ) times
 | ||||
|         tracker.trackFailure(); | ||||
|         tracker.trackFailure(); | ||||
| 
 | ||||
|         expect(failures.length).toBe(2, 'expected 2 failures to be tracked, got ' + failures.length); | ||||
|         expect(failures[0].failedEventId).toBe(decryptedEvent.getId(), 'the first failure should be tracked first'); | ||||
|         expect(failures[1].failedEventId).toBe(decryptedEvent2.getId(), 'the second failure should be tracked second'); | ||||
| 
 | ||||
|         done(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not track a failure for an event that was tracked previously', (done) => { | ||||
|         const decryptedEvent = createFailedDecryptionEvent(); | ||||
| 
 | ||||
|         const failures = []; | ||||
|         const tracker = new DecryptionFailureTracker((failure) => failures.push(failure)); | ||||
| 
 | ||||
|         // Indicate decryption
 | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
| 
 | ||||
|         // Pretend "now" is Infinity
 | ||||
|         tracker.checkFailures(Infinity); | ||||
| 
 | ||||
|         tracker.trackFailure(); | ||||
| 
 | ||||
|         // Indicate a second decryption, after having tracked the failure
 | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
| 
 | ||||
|         tracker.trackFailure(); | ||||
| 
 | ||||
|         expect(failures.length).toBe(1, 'should only track a single failure per event'); | ||||
| 
 | ||||
|         done(); | ||||
|     }); | ||||
| 
 | ||||
|     xit('should not track a failure for an event that was tracked in a previous session', (done) => { | ||||
|         // This test uses localStorage, clear it beforehand
 | ||||
|         localStorage.clear(); | ||||
| 
 | ||||
|         const decryptedEvent = createFailedDecryptionEvent(); | ||||
| 
 | ||||
|         const failures = []; | ||||
|         const tracker = new DecryptionFailureTracker((failure) => failures.push(failure)); | ||||
| 
 | ||||
|         // Indicate decryption
 | ||||
|         tracker.eventDecrypted(decryptedEvent); | ||||
| 
 | ||||
|         // Pretend "now" is Infinity
 | ||||
|         // NB: This saves to localStorage specific to DFT
 | ||||
|         tracker.checkFailures(Infinity); | ||||
| 
 | ||||
|         tracker.trackFailure(); | ||||
| 
 | ||||
|         // Simulate the browser refreshing by destroying tracker and creating a new tracker
 | ||||
|         const secondTracker = new DecryptionFailureTracker((failure) => failures.push(failure)); | ||||
| 
 | ||||
|         //secondTracker.loadTrackedEventHashMap();
 | ||||
| 
 | ||||
|         secondTracker.eventDecrypted(decryptedEvent); | ||||
|         secondTracker.checkFailures(Infinity); | ||||
|         secondTracker.trackFailure(); | ||||
| 
 | ||||
|         expect(failures.length).toBe(1, 'should track a single failure per event per session, got ' + failures.length); | ||||
| 
 | ||||
|         done(); | ||||
|     }); | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski