From 68fc04d9f6e6c6c84d7684dde9cfb8dc141c13b3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Apr 2019 13:45:20 -0600 Subject: [PATCH 1/8] Use the most recent version of the room in breadcrumbs Fixes https://github.com/vector-im/riot-web/issues/8662 --- src/components/views/rooms/RoomBreadcrumbs.js | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 314b2912cd..d68d7b6c4c 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -77,9 +77,9 @@ export default class RoomBreadcrumbs extends React.Component { const rooms = this.state.rooms.slice(); if (rooms.length) { - const {room, animated} = rooms[0]; - if (!animated) { - rooms[0] = {room, animated: true}; + const roomModel = rooms[0]; + if (!roomModel.animated) { + roomModel.animated = true; setTimeout(() => this.setState({rooms}), 0); } } @@ -159,16 +159,31 @@ export default class RoomBreadcrumbs extends React.Component { } _appendRoomId(roomId) { - const room = MatrixClientPeg.get().getRoom(roomId); - if (!room) { - return; - } + let room = MatrixClientPeg.get().getRoom(roomId); + if (!room) return; + const rooms = this.state.rooms.slice(); + + // If the room is upgraded, use that room instead. We'll also splice out + // any children of the room. + const history = MatrixClientPeg.get().getRoomUpgradeHistory(roomId); + if (history.length > 1) { + room = history[history.length - 1]; // Last room is most recent + + // Take out any room that isn't the most recent room + for (let i = 0; i < history.length - 1; i++) { + const idx = rooms.findIndex((r) => r.room.roomId === history[i].roomId); + if (idx !== -1) rooms.splice(idx, 1); + } + } + const existingIdx = rooms.findIndex((r) => r.room.roomId === room.roomId); if (existingIdx !== -1) { rooms.splice(existingIdx, 1); } + rooms.splice(0, 0, {room, animated: false}); + if (rooms.length > MAX_ROOMS) { rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS); } From 095e6a3ba6ac1de15683cfe4054900e16a064cb3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Apr 2019 14:31:23 -0600 Subject: [PATCH 2/8] Add an indicator to show a room is a direct chat Fixes https://github.com/vector-im/riot-web/issues/8797 --- res/css/views/rooms/_RoomBreadcrumbs.scss | 6 ++++++ src/components/views/rooms/RoomBreadcrumbs.js | 19 +++++++++++++++++++ src/i18n/strings/en_EN.json | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index 7e7ab5f23b..9770f8effc 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -41,6 +41,12 @@ limitations under the License. top: -3px; right: -4px; } + + .mx_RoomBreadcrumbs_dmIndicator { + position: absolute; + bottom: 0; + right: -4px; + } } .mx_RoomBreadcrumbs_animate { diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 314b2912cd..d2365a998e 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -24,6 +24,8 @@ import classNames from 'classnames'; import sdk from "../../../index"; import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from "../../../utils/FormattingUtils"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {_t} from "../../../languageHandler"; const MAX_ROOMS = 20; @@ -195,6 +197,11 @@ export default class RoomBreadcrumbs extends React.Component { this.setState({rooms}); } + _isDmRoom(room) { + const dmRooms = DMRoomMap.shared().getUserIdForRoomId(room.roomId); + return Boolean(dmRooms); + } + render() { const Tooltip = sdk.getComponent('elements.Tooltip'); const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar'); @@ -232,11 +239,23 @@ export default class RoomBreadcrumbs extends React.Component { badge =
{r.formattedCount}
; } + let dmIndicator; + if (this._isDmRoom(r.room)) { + dmIndicator = {_t("Direct; + } + return ( this._viewRoom(r.room)} onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}> {badge} + {dmIndicator} {tooltip} ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3dcc1691b8..7e6529e6f3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -760,6 +760,7 @@ "Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s", "Replying": "Replying", + "Direct Chat": "Direct Chat", "No rooms to show": "No rooms to show", "Unnamed room": "Unnamed room", "World readable": "World readable", @@ -1263,7 +1264,6 @@ "Forget": "Forget", "Favourite": "Favourite", "Low Priority": "Low Priority", - "Direct Chat": "Direct Chat", "Clear status": "Clear status", "Update status": "Update status", "Set status": "Set status", From 44198ea97dd229d95e5f8542b1aa84ff9642377c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Apr 2019 15:06:03 -0600 Subject: [PATCH 3/8] Sync breadcrumb rooms through account data Fixes https://github.com/vector-im/riot-web/issues/9315 Other clients would need to listen for and update im.vector.riot.breadcrumb_rooms in account data. --- src/components/views/rooms/RoomBreadcrumbs.js | 96 ++++++++++++++----- src/settings/Settings.js | 4 + .../handlers/AccountSettingsHandler.js | 18 ++++ 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 314b2912cd..60dae54e5c 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -24,6 +24,7 @@ import classNames from 'classnames'; import sdk from "../../../index"; import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from "../../../utils/FormattingUtils"; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; const MAX_ROOMS = 20; @@ -38,22 +39,22 @@ export default class RoomBreadcrumbs extends React.Component { componentWillMount() { this._dispatcherRef = dis.register(this.onAction); - const roomStr = localStorage.getItem("mx_breadcrumb_rooms"); - if (roomStr) { - try { - const roomIds = JSON.parse(roomStr); - this.setState({ - rooms: roomIds.map((r) => { - return { - room: MatrixClientPeg.get().getRoom(r), - animated: false, - }; - }).filter((r) => r.room), - }); - } catch (e) { - console.error("Failed to parse breadcrumbs:", e); + let storedRooms = SettingsStore.getValue("breadcrumb_rooms"); + if (!storedRooms || !storedRooms.length) { + // Fallback to the rooms stored in localstorage for those who would have had this. + // TODO: Remove this after a bit - the feature was only on develop, so a few weeks should be plenty time. + const roomStr = localStorage.getItem("mx_breadcrumb_rooms"); + if (roomStr) { + try { + storedRooms = JSON.parse(roomStr); + } catch (e) { + console.error("Failed to parse breadcrumbs:", e); + } } } + this._loadRoomIds(storedRooms || []); + + this._settingWatchRef = SettingsStore.watchSetting("breadcrumb_rooms", null, this.onBreadcrumbsChanged); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); @@ -64,6 +65,8 @@ export default class RoomBreadcrumbs extends React.Component { componentWillUnmount() { dis.unregister(this._dispatcherRef); + SettingsStore.unwatchSetting(this._settingWatchRef); + const client = MatrixClientPeg.get(); if (client) { client.removeListener("Room.myMembership", this.onMyMembership); @@ -84,8 +87,8 @@ export default class RoomBreadcrumbs extends React.Component { } } - const roomStr = JSON.stringify(rooms.map((r) => r.room.roomId)); - localStorage.setItem("mx_breadcrumb_rooms", roomStr); + const roomIds = rooms.map((r) => r.room.roomId); + SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds); } onAction(payload) { @@ -125,17 +128,48 @@ export default class RoomBreadcrumbs extends React.Component { } }; - _calculateRoomBadges(room) { - if (!room) return; + onBreadcrumbsChanged = (settingName, roomId, level, valueAtLevel, value) => { + if (!value) return; - const rooms = this.state.rooms.slice(); - const roomModel = rooms.find((r) => r.room.roomId === room.roomId); - if (!roomModel) return; // No applicable room, so don't do math on it + const currentState = this.state.rooms.map((r) => r.room.roomId); + if (currentState.length === value.length) { + let changed = false; + for (let i = 0; i < currentState.length; i++) { + if (currentState[i] !== value[i]) { + changed = true; + break; + } + } + if (!changed) return; + } + + this._loadRoomIds(value); + }; + + _loadRoomIds(roomIds) { + // If we're here, the list changed. + const rooms = roomIds.map((r) => MatrixClientPeg.get().getRoom(r)).filter((r) => r).map((r) => { + const badges = this._calculateBadgesForRoom(r) || {}; + return { + room: r, + animated: false, + ...badges, + }; + }); + this.setState({ + rooms: rooms, + }); + } + + _calculateBadgesForRoom(room) { + if (!room) return null; // Reset the notification variables for simplicity - roomModel.redBadge = false; - roomModel.formattedCount = "0"; - roomModel.showCount = false; + const roomModel = { + redBadge: false, + formattedCount: "0", + showCount: false, + }; const notifState = RoomNotifs.getRoomNotifsState(room.roomId); if (RoomNotifs.MENTION_BADGE_STATES.includes(notifState)) { @@ -155,6 +189,20 @@ export default class RoomBreadcrumbs extends React.Component { } } + return roomModel; + } + + _calculateRoomBadges(room) { + if (!room) return; + + const rooms = this.state.rooms.slice(); + const roomModel = rooms.find((r) => r.room.roomId === room.roomId); + if (!roomModel) return; // No applicable room, so don't do math on it + + const badges = this._calculateBadgesForRoom(room); + if (!badges) return; // No badges for some reason + + Object.assign(roomModel, badges); this.setState({rooms}); } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 6e17ffbbd7..098ef2c17b 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -258,6 +258,10 @@ export const SETTINGS = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: "en", }, + "breadcrumb_rooms": { + supportedLevels: ['account'], + default: [], + }, "analyticsOptIn": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td('Send analytics data'), diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index 675b2f8ead..71cef52c4e 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -19,6 +19,8 @@ import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; +const BREADCRUMBS_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms"; + /** * Gets and sets settings at the "account" level for the current user. * This handler does not make use of the roomId parameter. @@ -55,6 +57,9 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa const val = event.getContent()[settingName]; this._watchers.notifyUpdate(settingName, null, SettingLevel.ACCOUNT, val); } + } else if (event.getType() === BREADCRUMBS_EVENT_TYPE) { + const val = event.getContent()['rooms'] || []; + this._watchers.notifyUpdate("breadcrumb_rooms", null, SettingLevel.ACCOUNT, val); } } @@ -68,6 +73,12 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa return !content['disable']; } + // Special case for breadcrumbs + if (settingName === "breadcrumb_rooms") { + const content = this._getSettings(BREADCRUMBS_EVENT_TYPE) || {}; + return content['rooms'] || []; + } + const settings = this._getSettings() || {}; let preferredValue = settings[settingName]; @@ -89,6 +100,13 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", content); } + // Special case for breadcrumbs + if (settingName === "breadcrumb_rooms") { + const content = this._getSettings(BREADCRUMBS_EVENT_TYPE) || {}; + content['rooms'] = newValue; + return MatrixClientPeg.get().setAccountData(BREADCRUMBS_EVENT_TYPE, content); + } + const content = this._getSettings() || {}; content[settingName] = newValue; return MatrixClientPeg.get().setAccountData("im.vector.web.settings", content); From 406196e11c3e3f998f77d06acb946202aea4576f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Apr 2019 15:10:17 -0600 Subject: [PATCH 4/8] Move import to avoid future merge conflicts --- src/components/views/rooms/RoomBreadcrumbs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 60dae54e5c..0856ed892c 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -18,13 +18,13 @@ limitations under the License. import React from "react"; import dis from "../../../dispatcher"; import MatrixClientPeg from "../../../MatrixClientPeg"; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import RoomAvatar from '../avatars/RoomAvatar'; import classNames from 'classnames'; import sdk from "../../../index"; import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from "../../../utils/FormattingUtils"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; const MAX_ROOMS = 20; From 64a22236c3383f0879744b47c9e817e8b25ea51e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Apr 2019 16:21:57 -0600 Subject: [PATCH 5/8] Handle cases where the user rapidly clicks between rooms Once the user has breadcrumbs, there should always be breadcrumbs. Therefore it is safe to ignore any updates which have zero entries. --- src/components/views/rooms/RoomBreadcrumbs.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 0856ed892c..0950032aa2 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -88,7 +88,9 @@ export default class RoomBreadcrumbs extends React.Component { } const roomIds = rooms.map((r) => r.room.roomId); - SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds); + if (roomIds.length > 0) { + SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds); + } } onAction(payload) { @@ -147,6 +149,8 @@ export default class RoomBreadcrumbs extends React.Component { }; _loadRoomIds(roomIds) { + if (!roomIds || roomIds.length <= 0) return; // Skip updates with no rooms + // If we're here, the list changed. const rooms = roomIds.map((r) => MatrixClientPeg.get().getRoom(r)).filter((r) => r).map((r) => { const badges = this._calculateBadgesForRoom(r) || {}; From b7e557e49a7c5b8363ec2990f1944a5369a17eea Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Apr 2019 16:27:00 -0600 Subject: [PATCH 6/8] Autohide the scrollbar on breadcrumbs Fixes https://github.com/vector-im/riot-web/issues/9349 --- res/css/views/rooms/_RoomBreadcrumbs.scss | 7 ++++++- src/components/views/rooms/RoomBreadcrumbs.js | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index 7e7ab5f23b..715851f615 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -19,10 +19,15 @@ limitations under the License. height: 42px; padding: 8px; padding-bottom: 0; - overflow-x: visible; display: flex; flex-direction: row; + // Autohide the scrollbar + overflow-x: hidden; + &:hover { + overflow-x: visible; + } + .mx_AutoHideScrollbar_offset { display: flex; flex-direction: row; diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 314b2912cd..da3d4cf37d 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; import React from "react"; import dis from "../../../dispatcher"; import MatrixClientPeg from "../../../MatrixClientPeg"; From 04b521c48b51e20178ec9b0e581b596ebc077bd8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Apr 2019 17:03:06 -0600 Subject: [PATCH 7/8] Scroll breadcrumbs to the left when they change Fixes https://github.com/vector-im/riot-web/issues/9355 --- src/components/structures/IndicatorScrollbar.js | 7 +++++++ src/components/views/rooms/RoomBreadcrumbs.js | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 263a0a22ba..03e1f8db04 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -40,6 +40,13 @@ export default class IndicatorScrollbar extends React.Component { }; } + moveToOrigin() { + if (!this._scrollElement) return; + + this._scrollElement.scrollLeft = 0; + this._scrollElement.scrollTop = 0; + } + _collectScroller(scroller) { if (scroller && !this._scrollElement) { this._scrollElement = scroller; diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 314b2912cd..c76b174d4a 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -173,6 +173,10 @@ export default class RoomBreadcrumbs extends React.Component { rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS); } this.setState({rooms}); + + if (this.refs.scroller) { + this.refs.scroller.moveToOrigin(); + } } _viewRoom(room) { From c8ba7d34346808fcf17c551410db4bf7bd26cf18 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 5 Apr 2019 15:09:07 +0100 Subject: [PATCH 8/8] Fix a few bugs introduced in file upload rework * Fix the widget picture_snapshot command (not that I can find anything that uses it) * Remove unused prop * Fix plural on ContentMessages --- src/components/structures/RoomView.js | 4 +++- src/components/views/rooms/MessageComposerInput.js | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 47de3f2cb0..0ddbd06b4f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -536,7 +536,9 @@ module.exports = React.createClass({ payload.data.description || payload.data.name); break; case 'picture_snapshot': - this.uploadFile(payload.file); + return ContentMessages.sharedInstance().sendContentListToRoom( + [payload.file], this.state.room.roomId, MatrixClientPeg.get(), + ); break; case 'notifier_enabled': case 'upload_started': diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a0b66e40e1..f71e208eb8 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -47,7 +47,7 @@ import {Completion} from "../../../autocomplete/Autocompleter"; import Markdown from '../../../Markdown'; import ComposerHistoryManager from '../../../ComposerHistoryManager'; import MessageComposerStore from '../../../stores/MessageComposerStore'; -import ContentMessage from '../../../ContentMessages'; +import ContentMessages from '../../../ContentMessages'; import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix'; @@ -139,8 +139,6 @@ export default class MessageComposerInput extends React.Component { // js-sdk Room object room: PropTypes.object.isRequired, - onFilesPasted: PropTypes.func, - onInputStateChanged: PropTypes.func, }; @@ -1014,7 +1012,7 @@ export default class MessageComposerInput extends React.Component { // neither chrome nor firefox let you paste a plain file copied // from Finder) but more images copied from a different website // / word processor etc. - return ContentMessage.sharedInstance().sendContentListToRoom( + return ContentMessages.sharedInstance().sendContentListToRoom( transfer.files, this.props.room.roomId, this.client, ); case 'html': {