diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 498dfb8818..28e56e6e32 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -6,7 +6,6 @@ src/autocomplete/Autocompleter.js src/autocomplete/Components.js src/autocomplete/DuckDuckGoProvider.js src/autocomplete/EmojiProvider.js -src/autocomplete/RoomProvider.js src/autocomplete/UserProvider.js src/CallHandler.js src/component-index.js @@ -35,7 +34,6 @@ src/components/views/create_room/RoomAlias.js src/components/views/dialogs/ChatCreateOrReuseDialog.js src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/InteractiveAuthDialog.js -src/components/views/dialogs/SetMxIdDialog.js src/components/views/dialogs/UnknownDeviceDialog.js src/components/views/elements/AccessibleButton.js src/components/views/elements/ActionButton.js @@ -89,7 +87,6 @@ src/components/views/rooms/MemberList.js src/components/views/rooms/MemberTile.js src/components/views/rooms/MessageComposer.js src/components/views/rooms/MessageComposerInput.js -src/components/views/rooms/MessageComposerInputOld.js src/components/views/rooms/PresenceLabel.js src/components/views/rooms/ReadReceiptMarker.js src/components/views/rooms/RoomList.js @@ -100,7 +97,6 @@ src/components/views/rooms/RoomTile.js src/components/views/rooms/RoomTopicEditor.js src/components/views/rooms/SearchableEntityList.js src/components/views/rooms/SearchResultTile.js -src/components/views/rooms/TabCompleteBar.js src/components/views/rooms/TopUnreadMessagesBar.js src/components/views/rooms/UserTile.js src/components/views/settings/AddPhoneNumber.js @@ -128,8 +124,6 @@ src/Roles.js src/Rooms.js src/ScalarAuthClient.js src/ScalarMessaging.js -src/TabComplete.js -src/TabCompleteEntries.js src/TextForEvent.js src/Tinter.js src/UiEffects.js @@ -142,7 +136,7 @@ src/utils/Receipt.js src/Velociraptor.js src/VelocityBounce.js src/WhoIsTyping.js -src/wrappers/WithMatrixClient.js +src/wrappers/withMatrixClient.js test/all-tests.js test/components/structures/login/Registration-test.js test/components/structures/MessagePanel-test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bc4bbcfce..090f5a49da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,126 @@ +Changes in [0.10.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3) (2017-09-06) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.2...v0.10.3) + + * No changes + +Changes in [0.10.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.2) (2017-09-05) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.1...v0.10.3-rc.2) + + * Fix plurals in translations + [\#1358](https://github.com/matrix-org/matrix-react-sdk/pull/1358) + * Fix typo + [\#1357](https://github.com/matrix-org/matrix-react-sdk/pull/1357) + * Update from Weblate. + [\#1356](https://github.com/matrix-org/matrix-react-sdk/pull/1356) + +Changes in [0.10.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.1) (2017-09-01) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.2...v0.10.3-rc.1) + + * Fix room change sometimes being very slow + [\#1354](https://github.com/matrix-org/matrix-react-sdk/pull/1354) + * apply shouldHideEvent fn to onRoomTimeline for RoomStatusBar + [\#1346](https://github.com/matrix-org/matrix-react-sdk/pull/1346) + * text4event widget modified, used to show widget added each time. + [\#1345](https://github.com/matrix-org/matrix-react-sdk/pull/1345) + * separate concepts of showing and managing RRs to fix regression + [\#1352](https://github.com/matrix-org/matrix-react-sdk/pull/1352) + * Make staging widgets work with live and vice versa. + [\#1350](https://github.com/matrix-org/matrix-react-sdk/pull/1350) + * Avoid breaking /sync with uncaught exceptions + [\#1349](https://github.com/matrix-org/matrix-react-sdk/pull/1349) + * we need to pass whether it is an invite RoomSubList explicitly (i18n) + [\#1343](https://github.com/matrix-org/matrix-react-sdk/pull/1343) + * Percent encoding isn't a valid thing within _t + [\#1348](https://github.com/matrix-org/matrix-react-sdk/pull/1348) + * Fix spurious notifications + [\#1339](https://github.com/matrix-org/matrix-react-sdk/pull/1339) + * Unbreak password reset with a non-default HS + [\#1347](https://github.com/matrix-org/matrix-react-sdk/pull/1347) + * Remove unnecessary 'load' on notif audio element + [\#1341](https://github.com/matrix-org/matrix-react-sdk/pull/1341) + * _tJsx returns a React Object, the sub fn must return a React Object + [\#1340](https://github.com/matrix-org/matrix-react-sdk/pull/1340) + * Fix deprecation warning about promise.defer() + [\#1292](https://github.com/matrix-org/matrix-react-sdk/pull/1292) + * Fix click to insert completion + [\#1331](https://github.com/matrix-org/matrix-react-sdk/pull/1331) + +Changes in [0.10.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.2) (2017-08-24) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1...v0.10.2) + + * Force update on timelinepanel when event decrypted + [\#1334](https://github.com/matrix-org/matrix-react-sdk/pull/1334) + * Dispatch incoming_call synchronously + [\#1337](https://github.com/matrix-org/matrix-react-sdk/pull/1337) + * Fix React crying on machines without internet due to return undefined + [\#1335](https://github.com/matrix-org/matrix-react-sdk/pull/1335) + * Catch the promise rejection if scalar fails + [\#1333](https://github.com/matrix-org/matrix-react-sdk/pull/1333) + * Update from Weblate. + [\#1329](https://github.com/matrix-org/matrix-react-sdk/pull/1329) + +Changes in [0.10.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.1) (2017-08-23) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1-rc.1...v0.10.1) + + * [No changes] + +Changes in [0.10.1-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.1-rc.1) (2017-08-22) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.0-rc.2...v0.10.1-rc.1) + + * Matthew/multiple widgets + [\#1327](https://github.com/matrix-org/matrix-react-sdk/pull/1327) + * Fix proptypes on UserPickerDialog + [\#1326](https://github.com/matrix-org/matrix-react-sdk/pull/1326) + * AppsDrawer: Remove unnecessary bind + [\#1325](https://github.com/matrix-org/matrix-react-sdk/pull/1325) + * Position add app widget link + [\#1322](https://github.com/matrix-org/matrix-react-sdk/pull/1322) + * Remove app tile beta tag. + [\#1323](https://github.com/matrix-org/matrix-react-sdk/pull/1323) + * Add missing translation. + [\#1324](https://github.com/matrix-org/matrix-react-sdk/pull/1324) + * Note that apps are not E2EE + [\#1319](https://github.com/matrix-org/matrix-react-sdk/pull/1319) + * Only render appTile body (including warnings) if drawer shown. + [\#1321](https://github.com/matrix-org/matrix-react-sdk/pull/1321) + * Timeline improvements + [\#1320](https://github.com/matrix-org/matrix-react-sdk/pull/1320) + * Add a space between widget name and "widget" in widget event tiles + [\#1318](https://github.com/matrix-org/matrix-react-sdk/pull/1318) + * Move manage integrations button from settings page to room header as a + stand-alone component + [\#1286](https://github.com/matrix-org/matrix-react-sdk/pull/1286) + * Don't apply case logic to app names + [\#1316](https://github.com/matrix-org/matrix-react-sdk/pull/1316) + * Stop integ manager opening on every room switch + [\#1315](https://github.com/matrix-org/matrix-react-sdk/pull/1315) + * Add behaviour to toggle app draw on app tile header click + [\#1313](https://github.com/matrix-org/matrix-react-sdk/pull/1313) + * Change OOO so that MELS generation will continue over hidden events + [\#1308](https://github.com/matrix-org/matrix-react-sdk/pull/1308) + * Implement TextualEvent tiles for im.vector.modular.widgets + [\#1312](https://github.com/matrix-org/matrix-react-sdk/pull/1312) + * Don't show widget security warning to the person that added it to the room + [\#1314](https://github.com/matrix-org/matrix-react-sdk/pull/1314) + * remove unused strings introduced by string change + [\#1311](https://github.com/matrix-org/matrix-react-sdk/pull/1311) + * hotfix bad fn signature regression + [\#1310](https://github.com/matrix-org/matrix-react-sdk/pull/1310) + * Show a dialog if the maximum number of widgets allowed has been reached. + [\#1291](https://github.com/matrix-org/matrix-react-sdk/pull/1291) + * Fix Robot translation + [\#1309](https://github.com/matrix-org/matrix-react-sdk/pull/1309) + * Refactor ChatInviteDialog to be UserPickerDialog + [\#1300](https://github.com/matrix-org/matrix-react-sdk/pull/1300) + * Update Link to Translation status + [\#1302](https://github.com/matrix-org/matrix-react-sdk/pull/1302) + Changes in [0.9.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.7) (2017-06-22) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.6...v0.9.7) diff --git a/package.json b/package.json index 661db4b6bc..38a647ff67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.9.7", + "version": "0.10.3", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -66,7 +66,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "0.8.2", "optimist": "^0.6.1", "prop-types": "^15.5.8", "react": "^15.4.0", diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js new file mode 100644 index 0000000000..d6fbb460b5 --- /dev/null +++ b/src/ActiveRoomObserver.js @@ -0,0 +1,77 @@ +/* +Copyright 2017 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 RoomViewStore from './stores/RoomViewStore'; + +/** + * Consumes changes from the RoomViewStore and notifies specific things + * about when the active room changes. Unlike listening for RoomViewStore + * changes, you can subscribe to only changes relevant to a particular + * room. + * + * TODO: If we introduce an observer for something else, factor out + * the adding / removing of listeners & emitting into a common class. + */ +class ActiveRoomObserver { + constructor() { + this._listeners = {}; + + this._activeRoomId = RoomViewStore.getRoomId(); + // TODO: We could self-destruct when the last listener goes away, or at least + // stop listening. + this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); + } + + addListener(roomId, listener) { + if (!this._listeners[roomId]) this._listeners[roomId] = []; + this._listeners[roomId].push(listener); + } + + removeListener(roomId, listener) { + if (this._listeners[roomId]) { + const i = this._listeners[roomId].indexOf(listener); + if (i > -1) { + this._listeners[roomId].splice(i, 1); + } + } else { + console.warn("Unregistering unrecognised listener (roomId=" + roomId + ")"); + } + } + + _emit(roomId) { + if (!this._listeners[roomId]) return; + + for (const l of this._listeners[roomId]) { + l.call(); + } + } + + _onRoomViewStoreUpdate() { + // emit for the old room ID + if (this._activeRoomId) this._emit(this._activeRoomId); + + // update our cache + this._activeRoomId = RoomViewStore.getRoomId(); + + // and emit for the new one + if (this._activeRoomId) this._emit(this._activeRoomId); + } +} + +if (global.mx_ActiveRoomObserver === undefined) { + global.mx_ActiveRoomObserver = new ActiveRoomObserver(); +} +export default global.mx_ActiveRoomObserver; diff --git a/src/DateUtils.js b/src/DateUtils.js index 78eef57eae..77f3644f6f 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -65,7 +65,7 @@ module.exports = { const days = getDaysArray(); const months = getMonthsArray(); if (date.toDateString() === now.toDateString()) { - return this.formatTime(date); + return this.formatTime(date, showTwelveHour); } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { // TODO: use standard date localize function provided in counterpart return _t('%(weekDayName)s %(time)s', { @@ -78,7 +78,7 @@ module.exports = { weekDayName: days[date.getDay()], monthName: months[date.getMonth()], day: date.getDate(), - time: this.formatTime(date), + time: this.formatTime(date, showTwelveHour), }); } return this.formatFullDate(date, showTwelveHour); @@ -92,13 +92,13 @@ module.exports = { monthName: months[date.getMonth()], day: date.getDate(), fullYear: date.getFullYear(), - time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date), + time: this.formatTime(date, showTwelveHour), }); }, formatTime: function(date, showTwelveHour=false) { if (showTwelveHour) { - return twelveHourTime(date); + return twelveHourTime(date); } return pad(date.getHours()) + ':' + pad(date.getMinutes()); }, diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 87e714083b..671f9b4955 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,13 +32,28 @@ emojione.imagePathPNG = 'emojione/png/'; // Use SVGs for emojis emojione.imageType = 'svg'; +const SIMPLE_EMOJI_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; +/* + * Return true if the given string contains emoji + * Uses a much, much simpler regex than emojione's so will give false + * positives, but useful for fast-path testing strings to see if they + * need emojification. + * unicodeToImage uses this function. + */ +export function containsEmoji(str) { + return SIMPLE_EMOJI_PATTERN.test(str); +} + /* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js * because we want to include emoji shortnames in title text */ export function unicodeToImage(str) { + // fast path + if (!containsEmoji(str)) return str; + let replaceWith, unicode, alt, short, fname; const mappedUnicode = emojione.mapUnicodeToShort(); @@ -127,7 +143,7 @@ export function processHtmlForSending(html: string): string { * of that HTML. */ export function sanitizedHtmlNode(insaneHtml) { - const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); + const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); return
; } @@ -136,7 +152,7 @@ const sanitizeHtmlParams = { allowedTags: [ 'font', // custom to matrix for IRC-style font coloring 'del', // for markdown - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img', ], @@ -393,7 +409,6 @@ export function bodyToHtml(content, highlights, opts) { } safeBody = sanitizeHtml(body, sanitizeHtmlParams); safeBody = unicodeToImage(safeBody); - safeBody = addCodeCopyButton(safeBody); } finally { delete sanitizeHtmlParams.textFilter; @@ -412,23 +427,6 @@ export function bodyToHtml(content, highlights, opts) { return ; } -function addCodeCopyButton(safeBody) { - // Adds 'copy' buttons to pre blocks - // Note that this only manipulates the markup to add the buttons: - // we need to add the event handlers once the nodes are in the DOM - // since we can't save functions in the markup. - // This is done in TextualBody - const el = document.createElement("div"); - el.innerHTML = safeBody; - const codeBlocks = Array.from(el.getElementsByTagName("pre")); - codeBlocks.forEach(p => { - const button = document.createElement("span"); - button.className = "mx_EventTile_copyButton"; - p.appendChild(button); - }); - return el.innerHTML; -} - export function emojifyText(text) { return { __html: unicodeToImage(escape(text)), diff --git a/src/Markdown.js b/src/Markdown.js index 6e735c6f0e..455d5e95bd 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -17,7 +17,7 @@ limitations under the License. import commonmark from 'commonmark'; import escape from 'lodash/escape'; -const ALLOWED_HTML_TAGS = ['del', 'u']; +const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; diff --git a/src/Notifier.js b/src/Notifier.js index 1bb435307d..155564dcdf 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2017 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. @@ -33,9 +34,16 @@ import Modal from './Modal'; * } */ +const MAX_PENDING_ENCRYPTED = 20; + const Notifier = { notifsByRoom: {}, + // A list of event IDs that we've received but need to wait until + // they're decrypted until we decide whether to notify for them + // or not + pendingEncryptedEventIds: [], + notificationMessageForEvent: function(ev) { return TextForEvent.textForEvent(ev); }, @@ -89,17 +97,18 @@ const Notifier = { _playAudioNotification: function(ev, room) { const e = document.getElementById("messageAudio"); if (e) { - e.load(); e.play(); } }, start: function() { - this.boundOnRoomTimeline = this.onRoomTimeline.bind(this); + this.boundOnEvent = this.onEvent.bind(this); this.boundOnSyncStateChange = this.onSyncStateChange.bind(this); this.boundOnRoomReceipt = this.onRoomReceipt.bind(this); - MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); + this.boundOnEventDecrypted = this.onEventDecrypted.bind(this); + MatrixClientPeg.get().on('event', this.boundOnEvent); MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); + MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted); MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); this.toolbarHidden = false; this.isSyncing = false; @@ -107,8 +116,9 @@ const Notifier = { stop: function() { if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { - MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline); + MatrixClientPeg.get().removeListener('Event', this.boundOnEvent); MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); + MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); } this.isSyncing = false; @@ -237,23 +247,30 @@ const Notifier = { } }, - onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { - if (toStartOfTimeline) return; - if (!room) return; + onEvent: function(ev) { if (!this.isSyncing) return; // don't alert for any messages initially if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; - if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; - const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); - if (actions && actions.notify) { - if (this.isEnabled()) { - this._displayPopupNotification(ev, room); - } - if (actions.tweaks.sound && this.isAudioEnabled()) { - PlatformPeg.get().loudNotification(ev, room); - this._playAudioNotification(ev, room); + // If it's an encrypted event and the type is still 'm.room.encrypted', + // it hasn't yet been decrypted, so wait until it is. + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + this.pendingEncryptedEventIds.push(ev.getId()); + // don't let the list fill up indefinitely + while (this.pendingEncryptedEventIds.length > MAX_PENDING_ENCRYPTED) { + this.pendingEncryptedEventIds.shift(); } + return; } + + this._evaluateEvent(ev); + }, + + onEventDecrypted: function(ev) { + const idx = this.pendingEncryptedEventIds.indexOf(ev.getId()); + if (idx === -1) return; + + this.pendingEncryptedEventIds.splice(idx, 1); + this._evaluateEvent(ev); }, onRoomReceipt: function(ev, room) { @@ -273,6 +290,20 @@ const Notifier = { delete this.notifsByRoom[room.roomId]; } }, + + _evaluateEvent: function(ev) { + const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); + const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); + if (actions && actions.notify) { + if (this.isEnabled()) { + this._displayPopupNotification(ev, room); + } + if (actions.tweaks.sound && this.isAudioEnabled()) { + PlatformPeg.get().loudNotification(ev, room); + this._playAudioNotification(ev, room); + } + } + } }; if (!global.mxNotifier) { diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index b1d17b93a9..0b753cf3ab 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -76,10 +76,13 @@ class ScalarAuthClient { return defer.promise; } - getScalarInterfaceUrlForRoom(roomId, screen) { + getScalarInterfaceUrlForRoom(roomId, screen, id) { var url = SdkConfig.get().integrations_ui_url; url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "&room_id=" + encodeURIComponent(roomId); + if (id) { + url += '&integ_id=' + encodeURIComponent(id); + } if (screen) { url += '&screen=' + encodeURIComponent(screen); } diff --git a/src/Skinner.js b/src/Skinner.js index f47572ba01..1fe12f85ab 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -84,6 +84,9 @@ class Skinner { // behaviour with multiple copies of files etc. is erratic at best. // XXX: We can still end up with the same file twice in the resulting // JS bundle which is nonideal. +// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/ +// or https://nodejs.org/api/modules.html#modules_module_caching_caveats +// ("Modules are cached based on their resolved filename") if (global.mxSkinner === undefined) { global.mxSkinner = new Skinner(); } diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 95066912ac..36b8b538a7 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -244,15 +244,16 @@ function textForPowerEvent(event) { } return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { senderName: senderName, - powerLevelDiffText: diff.join(", ") + powerLevelDiffText: diff.join(", "), }); } function textForWidgetEvent(event) { - const senderName = event.sender ? event.sender.name : event.getSender(); - const previousContent = event.getPrevContent() || {}; + const senderName = event.getSender(); + const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); const {name, type, url} = event.getContent() || {}; - let widgetName = name || previousContent.name || type || previousContent.type || ''; + + let widgetName = name || prevName || type || prevType || ''; // Apply sentence case to widget name if (widgetName && widgetName.length > 0) { widgetName = widgetName[0].toUpperCase() + widgetName.slice(1) + ' '; @@ -261,9 +262,15 @@ function textForWidgetEvent(event) { // If the widget was removed, its content should be {}, but this is sufficiently // equivalent to that condition. if (url) { - return _t('%(widgetName)s widget added by %(senderName)s', { - widgetName, senderName, - }); + if (prevUrl) { + return _t('%(widgetName)s widget modified by %(senderName)s', { + widgetName, senderName, + }); + } else { + return _t('%(widgetName)s widget added by %(senderName)s', { + widgetName, senderName, + }); + } } else { return _t('%(widgetName)s widget removed by %(senderName)s', { widgetName, senderName, diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 3277c3a292..68973d17fc 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -81,10 +81,6 @@ export default React.createClass({ // stash the MatrixClient in case we log out before we are unmounted this._matrixClient = this.props.matrixClient; - // _scrollStateMap is a map from room id to the scroll state returned by - // RoomView.getScrollState() - this._scrollStateMap = {}; - CallMediaHandler.loadDevices(); document.addEventListener('keydown', this._onKeyDown); @@ -116,10 +112,6 @@ export default React.createClass({ return Boolean(MatrixClientPeg.get()); }, - getScrollStateForRoom: function(roomId) { - return this._scrollStateMap[roomId]; - }, - canResetTimelineInRoom: function(roomId) { if (!this.refs.roomView) { return true; @@ -248,7 +240,6 @@ export default React.createClass({ opacity={this.props.middleOpacity} collapsedRhs={this.props.collapse_rhs} ConferenceHandler={this.props.ConferenceHandler} - scrollStateMap={this._scrollStateMap} />; if (!this.props.collapse_rhs) right_panel = ; break; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index e8ac1a7f65..944ac37937 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -38,7 +38,6 @@ import linkifyMatrix from "../../linkify-matrix"; import * as Lifecycle from '../../Lifecycle'; // LifecycleStore is not used but does listen to and dispatch actions require('../../stores/LifecycleStore'); -import RoomViewStore from '../../stores/RoomViewStore'; import PageTypes from '../../PageTypes'; import createRoom from "../../createRoom"; @@ -214,9 +213,6 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); - this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated); - this._onRoomViewStoreUpdated(); - if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable(); // Used by _viewRoom before getting state from sync @@ -353,7 +349,6 @@ module.exports = React.createClass({ UDEHandler.stopListening(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); - this._roomViewStoreToken.remove(); }, componentDidUpdate: function() { @@ -587,10 +582,6 @@ module.exports = React.createClass({ } }, - _onRoomViewStoreUpdated: function() { - this.setState({ currentRoomId: RoomViewStore.getRoomId() }); - }, - _setPage: function(pageType) { this.setState({ page_type: pageType, @@ -677,6 +668,7 @@ module.exports = React.createClass({ this.focusComposer = true; const newState = { + currentRoomId: roomInfo.room_id || null, page_type: PageTypes.RoomView, thirdPartyInvite: roomInfo.third_party_invite, roomOobData: roomInfo.oob_data, @@ -1068,10 +1060,13 @@ module.exports = React.createClass({ self.setState({ready: true}); }); cli.on('Call.incoming', function(call) { + // we dispatch this synchronously to make sure that the event + // handlers on the call are set up immediately (so that if + // we get an immediate hangup, we don't get a stuck call) dis.dispatch({ action: 'incoming_call', call: call, - }); + }, true); }); cli.on('Session.logged_out', function(call) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 460ed43e82..e5884973c6 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -65,7 +65,7 @@ module.exports = React.createClass({ suppressFirstDateSeparator: React.PropTypes.bool, // whether to show read receipts - manageReadReceipts: React.PropTypes.bool, + showReadReceipts: React.PropTypes.bool, // true if updates to the event list should cause the scroll panel to // scroll down when we are at the bottom of the window. See ScrollPanel @@ -491,7 +491,7 @@ module.exports = React.createClass({ var scrollToken = mxEv.status ? undefined : eventId; var readReceipts; - if (this.props.manageReadReceipts) { + if (this.props.showReadReceipts) { readReceipts = this._getReadReceiptsForEvent(mxEv); } ret.push( diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 0b8055beda..624f3c67dd 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -125,7 +125,7 @@ export default withMatrixClient(React.createClass({ {_tJsx( - 'To join an exisitng group you\'ll have to '+ + 'To join an existing group you\'ll have to '+ 'know its group identifier; this will look '+ 'something like +example:matrix.org.', /(.*)<\/i>/, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f825d1efbb..2a6cf0aee4 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -20,6 +20,8 @@ limitations under the License. // - Drag and drop // - File uploading - uploadFile() +import shouldHideEvent from "../../shouldHideEvent"; + var React = require("react"); var ReactDOM = require("react-dom"); import Promise from 'bluebird'; @@ -45,6 +47,7 @@ import KeyCode from '../../KeyCode'; import UserProvider from '../../autocomplete/UserProvider'; import RoomViewStore from '../../stores/RoomViewStore'; +import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; let DEBUG = false; let debuglog = function() {}; @@ -143,6 +146,8 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); + this._syncedSettings = UserSettingsStore.getSyncedSettings(); + // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); @@ -152,6 +157,22 @@ module.exports = React.createClass({ if (this.unmounted) { return; } + + if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) { + // RoomView explicitly does not support changing what room + // is being viewed: instead it should just be re-mounted when + // switching rooms. Therefore, if the room ID changes, we + // ignore this. We either need to do this or add code to handle + // saving the scroll position (otherwise we end up saving the + // scroll position against the wrong room). + + // Given that doing the setState here would cause a bunch of + // unnecessary work, we just ignore the change since we know + // that if the current room ID has changed from what we thought + // it was, it means we're about to be unmounted. + return; + } + const newState = { roomId: RoomViewStore.getRoomId(), roomAlias: RoomViewStore.getRoomAlias(), @@ -159,7 +180,6 @@ module.exports = React.createClass({ roomLoadError: RoomViewStore.getRoomLoadError(), joining: RoomViewStore.isJoining(), initialEventId: RoomViewStore.getInitialEventId(), - initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), forwardingEvent: RoomViewStore.getForwardingEvent(), shouldPeek: RoomViewStore.shouldPeek(), @@ -185,6 +205,25 @@ module.exports = React.createClass({ // the RoomView instance if (initial) { newState.room = MatrixClientPeg.get().getRoom(newState.roomId); + if (newState.room) { + newState.unsentMessageError = this._getUnsentMessageError(newState.room); + newState.showApps = this._shouldShowApps(newState.room); + this._onRoomLoaded(newState.room); + } + } + + if (this.state.roomId === null && newState.roomId !== null) { + // Get the scroll state for the new room + + // If an event ID wasn't specified, default to the one saved for this room + // in the scroll state store. Assume initialEventPixelOffset should be set. + if (!newState.initialEventId) { + const roomScrollState = RoomScrollStateStore.getScrollState(newState.roomId); + if (roomScrollState) { + newState.initialEventId = roomScrollState.focussedEvent; + newState.initialEventPixelOffset = roomScrollState.pixelOffset; + } + } } // Clear the search results when clicking a search result (which changes the @@ -193,22 +232,20 @@ module.exports = React.createClass({ newState.searchResults = null; } - // Store the scroll state for the previous room so that we can return to this - // position when viewing this room in future. - if (this.state.roomId !== newState.roomId) { - this._updateScrollMap(this.state.roomId); - } + this.setState(newState); + // At this point, newState.roomId could be null (e.g. the alias might not + // have been resolved yet) so anything called here must handle this case. - this.setState(newState, () => { - // At this point, this.state.roomId could be null (e.g. the alias might not - // have been resolved yet) so anything called here must handle this case. - if (initial) { - this._onHaveRoom(); - } - }); + // We pass the new state into this function for it to read: it needs to + // observe the new state but we don't want to put it in the setState + // callback because this would prevent the setStates from being batched, + // ie. cause it to render RoomView twice rather than the once that is necessary. + if (initial) { + this._setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek); + } }, - _onHaveRoom: function() { + _setupRoom: function(room, roomId, joining, shouldPeek) { // if this is an unknown room then we're in one of three states: // - This is a room we can peek into (search engine) (we can /peek) // - This is a room we can publicly join or were invited to. (we can /join) @@ -224,23 +261,15 @@ module.exports = React.createClass({ // about it). We don't peek in the historical case where we were joined but are // now not joined because the js-sdk peeking API will clobber our historical room, // making it impossible to indicate a newly joined room. - const room = this.state.room; - if (room) { - this.setState({ - unsentMessageError: this._getUnsentMessageError(room), - showApps: this._shouldShowApps(room), - }); - this._onRoomLoaded(room); - } - if (!this.state.joining && this.state.roomId) { + if (!joining && roomId) { if (this.props.autoJoin) { this.onJoinButtonClicked(); - } else if (!room && this.state.shouldPeek) { - console.log("Attempting to peek into room %s", this.state.roomId); + } else if (!room && shouldPeek) { + console.log("Attempting to peek into room %s", roomId); this.setState({ peekLoading: true, }); - MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => { + MatrixClientPeg.get().peekInRoom(roomId).then((room) => { this.setState({ room: room, peekLoading: false, @@ -336,7 +365,9 @@ module.exports = React.createClass({ this.unmounted = true; // update the scroll map before we get unmounted - this._updateScrollMap(this.state.roomId); + if (this.state.roomId) { + RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState()); + } if (this.refs.roomView) { // disconnect the D&D event listeners from the room view. This @@ -497,8 +528,7 @@ module.exports = React.createClass({ // update unread count when scrolled up if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { // no change - } - else { + } else if (!shouldHideEvent(ev, this._syncedSettings)) { this.setState((state, props) => { return {numUnreadMessages: state.numUnreadMessages + 1}; }); @@ -614,18 +644,6 @@ module.exports = React.createClass({ }); }, - _updateScrollMap(roomId) { - // No point updating scroll state if the room ID hasn't been resolved yet - if (!roomId) { - return; - } - dis.dispatch({ - action: 'update_scroll_state', - room_id: roomId, - scroll_state: this._getScrollState(), - }); - }, - onRoom: function(room) { if (!room || room.roomId !== this.state.roomId) { return; @@ -1716,7 +1734,8 @@ module.exports = React.createClass({ var messagePanel = (