diff --git a/karma.conf.js b/karma.conf.js index 41ddbdf249..4d699599cb 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -199,25 +199,12 @@ module.exports = function (config) { 'matrix-react-sdk': path.resolve('test/skinned-sdk.js'), 'sinon': 'sinon/pkg/sinon.js', - - // To make webpack happy - // Related: https://github.com/request/request/issues/1529 - // (there's no mock available for fs, so we fake a mock by using - // an in-memory version of fs) - "fs": "memfs", }, modules: [ path.resolve('./test'), "node_modules" ], }, - node: { - // Because webpack is made of fail - // https://github.com/request/request/issues/1529 - // Note: 'mock' is the new 'empty' - net: 'mock', - tls: 'mock' - }, devtool: 'inline-source-map', externals: { // Don't try to bundle electron: leave it as a commonjs dependency diff --git a/package.json b/package.json index b72080cd36..8a51c0877d 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "lodash": "^4.13.1", "lolex": "2.3.2", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", - "memfs": "^2.10.1", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", diff --git a/res/css/structures/_AutoHideScrollbar.scss b/res/css/structures/_AutoHideScrollbar.scss index 0b8558a61e..70dbbe8d07 100644 --- a/res/css/structures/_AutoHideScrollbar.scss +++ b/res/css/structures/_AutoHideScrollbar.scss @@ -18,6 +18,7 @@ limitations under the License. 1. for browsers that support native overlay auto-hiding scrollbars */ .mx_AutoHideScrollbar { + overflow-x: hidden; overflow-y: auto; -ms-overflow-style: -ms-autohiding-scrollbar; } @@ -34,23 +35,20 @@ body.mx_scrollbar_overlay_noautohide .mx_AutoHideScrollbar:hover { } /* 3. as a last fallback, compensate for the scrollbar taking up space in the layout -by playing with the paddings. the default below will add a right padding -of the scrollbar width and clear that on hover. -this won't work well on classes that also need to set their padding, -so this needs to be overriden and adjust the padding with calc like so: -``` -body.mx_scrollbar_nooverlay .componentClass.mx_AutoHideScrollbar_overflow:hover { - padding-right: calc(15px - var(--scrollbar-width)) !important; -} -``` +by having giving the child element (.mx_AutoHideScrollbar_offset) a +negative right margin of the width of the scrollbar when the container +is overflowing. This is what Firefox ends up using. Overflow is detected +in javascript, and adds the mx_AutoHideScrollbar_overflow class to the container. +This only works in Firefox, which should be fine as this fallback is only needed there. */ body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar { - box-sizing: border-box; overflow-y: hidden; - padding-right: var(--scrollbar-width); } body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover { overflow-y: auto; - padding-right: 0; +} + +body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow > .mx_AutoHideScrollbar_offset { + margin-right: calc(-1 * var(--scrollbar-width)); } diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 30ce90a84f..142a43af95 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -22,98 +22,74 @@ limitations under the License. } .mx_RoomSubList_nonEmpty { - margin-bottom: 8px; + margin-bottom: 4px; } .mx_RoomSubList_labelContainer { display: flex; flex-direction: row; + align-items: center; flex: 0 0 auto; - margin: 8px 19px 0 0; + margin: 0 16px; + height: 36px; } .mx_RoomSubList_label { flex: 1; - position: relative; + cursor: pointer; + display: flex; + align-items: center; + padding: 0 6px; +} + +.mx_RoomSubList_label > span { + flex: 1 1 auto; text-transform: uppercase; color: $roomsublist-label-fg-color; font-weight: 700; font-size: 12px; - margin-left: 16px; - padding-left: 16px; /* gutter */ - padding-right: 16px; /* gutter */ - padding-top: 6px; - padding-bottom: 6px; - cursor: pointer; -} - -.mx_RoomSubList_label.mx_RoomSubList_fixed { - position: fixed; - top: 0; - z-index: 5; - /* pointer-events: none; */ + margin-left: 8px; } .mx_RoomSubList_badge { - height: 18px; - border-radius: 9px; + flex: 0 0 auto; + border-radius: 8px; color: $accent-fg-color; font-weight: 600; font-size: 12px; - vertical-align: middle; - line-height: 18px; - padding: 0 4px; + padding: 0 5px; background-color: $accent-color; } -.mx_RoomSubList_label .mx_RoomSubList_badge:hover { - filter: brightness($focus-brightness); -} - .mx_RoomSubList_addRoom, .mx_RoomSubList_badge { - margin: 5px; + margin-left: 7px; } .mx_RoomSubList_addRoom { background-color: $roomheader-addroom-color; color: $roomsublist-background; - border-radius: 9px; - text-align: center; - vertical-align: middle; - line-height: 18px; - font-weight: bold; - font-size: 18px; - width: 18px; - height: 18px; + background-image: url('../../img/icons-room-add.svg'); + background-repeat: no-repeat; + background-position: center; + border-radius: 10px; // 16/2 + 2 padding + height: 16px; + flex: 0 0 16px; + background-clip: content-box; } .mx_RoomSubList_badgeHighlight { background-color: $warning-color; } -/* This is the bottom of the speech bubble */ -.mx_RoomSubList_badgeHighlight:after { - content: ""; - position: absolute; - display: block; - width: 0; - height: 0; - margin-left: 5px; - border-top: 5px solid $warning-color; - border-right: 7px solid transparent; -} - .mx_RoomSubList_chevron { - left: 0px; pointer-events: none; - position: absolute; - top: 11px; - width: 9px; - height: 4px; background-image: url('../../img/topleft-chevron.svg'); - background-size: cover; - // the transition doesn't work as the chevron gets remounted - transition: rotateZ 0.2s ease-in; + background-repeat: no-repeat; + transition: transform 0.2s ease-in; + width: 10px; + height: 10px; + background-position: center; + margin-left: 2px; } .mx_RoomSubList_chevronDown { @@ -131,47 +107,26 @@ limitations under the License. .mx_RoomSubList_scroll { /* let rooms list grab all available space */ flex: 0 1 auto; - padding: 0 15px !important; -} -/* -for browsers that don't support overlay scrollbars, -subtract scrollbar width from right padding on hover when overflowing -so the content doesn't jump when showing the scrollbars -*/ -body.mx_scrollbar_nooverlay .mx_RoomSubList_scroll.mx_AutoHideScrollbar_overflow:hover { - padding-right: calc(15px - var(--scrollbar-width)) !important; + padding: 0 8px; } .collapsed { - .mx_RoomSubList_label { - height: 17px; - width: 28px; /* collapsed LHS Panel width */ + + .mx_RoomSubList_scroll { + padding: 0; } .mx_RoomSubList_labelContainer { - width: 28px; /* collapsed LHS Panel width */ + margin-right: 14px; + margin-left: 2px; } - /* Hide the bottom of speech bubble */ - .mx_RoomSubList_badgeHighlight:after { - display: none; + .mx_RoomSubList_addRoom { + margin-left: 3px; + margin-right: 28px; } - .mx_RoomSubList_line { - display: none; - } - - .mx_RoomSubList_moreBadge { - position: static; - margin-left: 16px; - margin-top: 2px; - } - - .mx_RoomSubList_ellipsis { - height: 20px; - } - - .mx_RoomSubList_more { + .mx_RoomSubList_label > span { display: none; } } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 475d2ac275..ff54da7196 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -21,10 +21,30 @@ limitations under the License. cursor: pointer; height: 40px; margin: 0; - padding: 2px 12px; + padding: 0 8px 0 10px; position: relative; } +.mx_RoomTile_menuButton { + display: none; + flex: 0 0 16px; + height: 16px; + background-image: url('../../img/icon_context.svg'); + background-repeat: no-repeat; + background-position: center; +} + +// toggle menuButton and badge on hover/menu displayed +.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover, .mx_RoomTile_menuDisplayed { + .mx_RoomTile_menuButton { + display: block; + } + + .mx_RoomTile_badge { + display: none; + } +} + .mx_RoomTile_tooltip { display: inline-block; position: relative; @@ -62,14 +82,12 @@ limitations under the License. text-overflow: ellipsis; } -.mx_RoomTile_invite { -/* color: rgba(69, 69, 69, 0.5); */ -} - .collapsed { .mx_RoomTile { - margin: 2px; - padding: 2px 0 2px 12px; + margin: 0 2px; + padding: 0 2px; + position: relative; + justify-content: center; } .mx_RoomTile_name { @@ -77,57 +95,26 @@ limitations under the License. } .mx_RoomTile_badge { - display: block; position: absolute; - height: 15px; - right: 8px; - top: 2px; - min-width: 12px; + right: 6px; + top: 0px; border-radius: 16px; - padding: 0px 4px 0px 4px; z-index: 3; + border: 0.18em solid $secondary-accent-color; } - /* Hide the bottom of speech bubble */ - .mx_RoomTile_highlight .mx_RoomTile_badge:after { - display: none; + .mx_RoomTile_menuButton { + display: none; //no design for this for now } } -/* This is the bottom of the speech bubble */ -.mx_RoomTile_highlight .mx_RoomTile_badge:after { - content: ""; - position: absolute; - display: block; - width: 0; - height: 0; - margin-left: 5px; - border-top: 5px solid $warning-color; - border-right: 7px solid transparent; -} - .mx_RoomTile_badge { flex: 0 1 content; - min-width: 15px; - border-radius: 8px; + border-radius: 0.8em; + padding: 0 0.4em; color: $accent-fg-color; font-weight: 600; font-size: 12px; - text-align: center; - padding-top: 1px; - padding-left: 4px; - padding-right: 4px; -} - -.mx_RoomTile .mx_RoomTile_badge.mx_RoomTile_badgeButton, -.mx_RoomTile.mx_RoomTile_menuDisplayed .mx_RoomTile_badge { - letter-spacing: 0.1em; - opacity: 1; -} - -.mx_RoomTile.mx_RoomTile_noBadges .mx_RoomTile_badge.mx_RoomTile_badgeButton, -.mx_RoomTile.mx_RoomTile_menuDisplayed.mx_RoomTile_noBadges .mx_RoomTile_badge { - background-color: $neutral-badge-color; } .mx_RoomTile_unreadNotify .mx_RoomTile_badge { @@ -169,10 +156,6 @@ limitations under the License. background-color: $roomtile-focused-bg-color; } -.mx_RoomTile .mx_RoomTile_name.mx_RoomTile_badgeShown { - width: 140px; -} - .mx_RoomTile_arrow { position: absolute; right: 0px; diff --git a/res/img/icons-room-add.svg b/res/img/icons-room-add.svg index fc0ab750b6..6dd2e21295 100644 --- a/res/img/icons-room-add.svg +++ b/res/img/icons-room-add.svg @@ -1,23 +1,71 @@ - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/res/img/topleft-chevron.svg b/res/img/topleft-chevron.svg index 9fbf5fe9ca..fa89852874 100644 --- a/res/img/topleft-chevron.svg +++ b/res/img/topleft-chevron.svg @@ -1,17 +1,86 @@ - - - - dropdown - Created with Sketch. - - - - - - - - - + + + + + + image/svg+xml + + dropdown + + + + + + dropdown + Created with Sketch. + + + + + + + + - \ No newline at end of file + + diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index 31bcac3e52..c1d42ffd0d 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -62,6 +62,35 @@ function createAccountDataAction(matrixClient, accountDataEvent) { }; } +/** + * @typedef RoomAccountDataAction + * @type {Object} + * @property {string} action 'MatrixActions.Room.accountData'. + * @property {MatrixEvent} event the MatrixEvent that triggered the dispatch. + * @property {string} event_type the type of the MatrixEvent, e.g. "m.direct". + * @property {Object} event_content the content of the MatrixEvent. + * @property {Room} room the room where the account data was changed. + */ + +/** + * Create a MatrixActions.Room.accountData action that represents a MatrixClient `Room.accountData` + * matrix event. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} accountDataEvent the account data event. + * @param {Room} room the room where account data was changed + * @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData. + */ +function createRoomAccountDataAction(matrixClient, accountDataEvent, room) { + return { + action: 'MatrixActions.Room.accountData', + event: accountDataEvent, + event_type: accountDataEvent.getType(), + event_content: accountDataEvent.getContent(), + room: room, + }; +} + /** * @typedef RoomAction * @type {Object} @@ -201,6 +230,7 @@ export default { start(matrixClient) { this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction); this._addMatrixClientListener(matrixClient, 'Room', createRoomAction); this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js index 507af2c5f0..a462b2bf14 100644 --- a/src/components/structures/AutoHideScrollbar.js +++ b/src/components/structures/AutoHideScrollbar.js @@ -112,7 +112,9 @@ export default class AutoHideScrollbar extends React.Component { ref={this._collectContainerRef} className={["mx_AutoHideScrollbar", this.props.className].join(" ")} > - { this.props.children } +
+ { this.props.children } +
); } } diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f2c7f08f0d..77f6f1f948 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -151,8 +151,7 @@ const LeftPanel = React.createClass({ } } while (element && !( classes.contains("mx_RoomTile") || - classes.contains("mx_SearchBox_search") || - classes.contains("mx_RoomSubList_ellipsis"))); + classes.contains("mx_SearchBox_search"))); if (element) { element.focus(); diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index f99c4bf90b..0bebc50da6 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -243,9 +243,8 @@ const RoomSubList = React.createClass({ const subListNotifCount = subListNotifications[0]; const subListNotifHighlight = subListNotifications[1]; - let badge; - if (this.state.hidden) { + if (!this.props.collapsed) { const badgeClasses = classNames({ 'mx_RoomSubList_badge': true, 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, @@ -285,9 +284,7 @@ const RoomSubList = React.createClass({ let addRoomButton; if (this.props.onAddRoom) { addRoomButton = ( - - + - + ); } @@ -307,7 +304,7 @@ const RoomSubList = React.createClass({
{ chevron } - { this.props.collapsed ? '' : this.props.label } + {this.props.label} { incomingCall } { badge } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 7440d42cb4..83dc29a08e 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -82,6 +82,8 @@ const SIMPLE_SETTINGS = [ { id: "TagPanel.disableTagPanel" }, { id: "enableWidgetScreenshots" }, { id: "RoomSubList.showEmpty" }, + { id: "pinMentionedRooms" }, + { id: "pinUnreadRooms" }, { id: "showDeveloperTools" }, ]; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 1afff783a1..faa08c7001 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -220,7 +220,7 @@ module.exports = React.createClass({ this.setState( { badgeHover: false } ); }, - onBadgeClicked: function(e) { + onOpenMenu: function(e) { // Prevent the RoomTile onClick event firing as well e.stopPropagation(); // Only allow non-guests to access the context menu @@ -276,19 +276,14 @@ module.exports = React.createClass({ if (name == undefined || name == null) name = ''; name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - let badgeContent; - if (this.state.badgeHover || this.state.menuDisplayed) { - badgeContent = "\u00B7\u00B7\u00B7"; - } else if (badges) { + let badge; + if (badges) { const limitedCount = FormattingUtils.formatCount(notificationCount); - badgeContent = notificationCount ? limitedCount : '!'; - } else { - badgeContent = '\u200B'; + const badgeContent = notificationCount ? limitedCount : '!'; + badge =
{ badgeContent }
; } - const badge =
{ badgeContent }
; - const EmojiText = sdk.getComponent('elements.EmojiText'); let label; let tooltip; @@ -317,6 +312,11 @@ module.exports = React.createClass({ // incomingCallBox = ; //} + let contextMenuButton; + if (!MatrixClientPeg.get().isGuest()) { + contextMenuButton = ; + } + const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); let dmIndicator; @@ -338,6 +338,7 @@ module.exports = React.createClass({
{ label } + { contextMenuButton } { badge } { /* { incomingCallBox } */ } { tooltip } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 96d807b851..d0b34296bf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -249,6 +249,8 @@ "Enable URL previews for this room (only affects you)": "Enable URL previews for this room (only affects you)", "Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room", "Room Colour": "Room Colour", + "Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list", + "Pin rooms I'm mentioned in to the top of the room list": "Pin rooms I'm mentioned in to the top of the room list", "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Show empty room list headings": "Show empty room list headings", "Collecting app version information": "Collecting app version information", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 547c71bac8..d65303b7c6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -276,6 +276,16 @@ export const SETTINGS = { default: true, controller: new AudioNotificationsEnabledController(), }, + "pinMentionedRooms": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Pin rooms I'm mentioned in to the top of the room list"), + default: false, + }, + "pinUnreadRooms": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Pin unread rooms to the top of the room list"), + default: false, + }, "enableWidgetScreenshots": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable widget screenshots on supported widgets'), diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index c670161dbc..0f8e5d7b4d 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -17,6 +17,7 @@ import {Store} from 'flux/utils'; import dis from '../dispatcher'; import DMRoomMap from '../utils/DMRoomMap'; import Unread from '../Unread'; +import SettingsStore from "../settings/SettingsStore"; /** * A class for storing application state for categorising rooms in @@ -53,6 +54,24 @@ class RoomListStore extends Store { "im.vector.fake.archived": [], }, ready: false, + + // The room cache stores a mapping of roomId to cache record. + // Each cache record is a key/value pair for various bits of + // data used to sort the room list. Currently this stores the + // following bits of informations: + // "timestamp": number, The timestamp of the last relevant + // event in the room. + // "notifications": boolean, Whether or not the user has been + // highlighted on any unread events. + // "unread": boolean, Whether or not the user has any + // unread events. + // + // All of the cached values are lazily loaded on read in the + // recents comparator. When an event is received for a particular + // room, all the cached values are invalidated - forcing the + // next read to set new values. The entries do not expire on + // their own. + roomCache: {}, }; } @@ -84,6 +103,8 @@ class RoomListStore extends Store { !payload.isLiveUnfilteredRoomTimelineEvent || !this._eventTriggersRecentReorder(payload.event) ) break; + + this._clearCachedRoomState(payload.event.getRoomId()); this._generateRoomLists(); } break; @@ -111,6 +132,8 @@ class RoomListStore extends Store { if (liveTimeline !== eventTimeline || !this._eventTriggersRecentReorder(payload.event) ) break; + + this._clearCachedRoomState(payload.event.getRoomId()); this._generateRoomLists(); } break; @@ -119,6 +142,13 @@ class RoomListStore extends Store { this._generateRoomLists(); } break; + case 'MatrixActions.Room.accountData': { + if (payload.event_type === 'm.fully_read') { + this._clearCachedRoomState(payload.room.roomId); + this._generateRoomLists(); + } + } + break; case 'MatrixActions.Room.myMembership': { this._generateRoomLists(); } @@ -216,11 +246,18 @@ class RoomListStore extends Store { } }); + // Note: we check the settings up here instead of in the forEach or + // in the _recentsComparator to avoid hitting the SettingsStore a few + // thousand times. + const pinUnread = SettingsStore.getValue("pinUnreadRooms"); + const pinMentioned = SettingsStore.getValue("pinMentionedRooms"); Object.keys(lists).forEach((listKey) => { let comparator; switch (RoomListStore._listOrders[listKey]) { case "recent": - comparator = this._recentsComparator; + comparator = (roomA, roomB) => { + return this._recentsComparator(roomA, roomB, pinUnread, pinMentioned); + }; break; case "manual": default: @@ -236,6 +273,44 @@ class RoomListStore extends Store { }); } + _updateCachedRoomState(roomId, type, value) { + const roomCache = this._state.roomCache; + if (!roomCache[roomId]) roomCache[roomId] = {}; + + if (value) roomCache[roomId][type] = value; + else delete roomCache[roomId][type]; + + this._setState({roomCache}); + } + + _clearCachedRoomState(roomId) { + const roomCache = this._state.roomCache; + delete roomCache[roomId]; + this._setState({roomCache}); + } + + _getRoomState(room, type) { + const roomId = room.roomId; + const roomCache = this._state.roomCache; + if (roomCache[roomId] && typeof roomCache[roomId][type] !== 'undefined') { + return roomCache[roomId][type]; + } + + if (type === "timestamp") { + const ts = this._tsOfNewestEvent(room); + this._updateCachedRoomState(roomId, "timestamp", ts); + return ts; + } else if (type === "unread") { + const unread = room.getUnreadNotificationCount() > 0; + this._updateCachedRoomState(roomId, "unread", unread); + return unread; + } else if (type === "notifications") { + const notifs = room.getUnreadNotificationCount("highlight") > 0; + this._updateCachedRoomState(roomId, "notifications", notifs); + return notifs; + } else throw new Error("Unrecognized room cache type: " + type); + } + _eventTriggersRecentReorder(ev) { return ev.getTs() && ( Unread.eventTriggersUnreadCount(ev) || @@ -261,10 +336,40 @@ class RoomListStore extends Store { } } - _recentsComparator(roomA, roomB) { - // XXX: We could use a cache here and update it when we see new - // events that trigger a reorder - return this._tsOfNewestEvent(roomB) - this._tsOfNewestEvent(roomA); + _recentsComparator(roomA, roomB, pinUnread, pinMentioned) { + // We try and set the ordering to be Mentioned > Unread > Recent + // assuming the user has the right settings, of course. + + const timestampA = this._getRoomState(roomA, "timestamp"); + const timestampB = this._getRoomState(roomB, "timestamp"); + const timestampDiff = timestampB - timestampA; + + if (pinMentioned) { + const mentionsA = this._getRoomState(roomA, "notifications"); + const mentionsB = this._getRoomState(roomB, "notifications"); + if (mentionsA && !mentionsB) return -1; + if (!mentionsA && mentionsB) return 1; + + // If they both have notifications, sort by timestamp. + // If neither have notifications (the fourth check not shown + // here), then try and sort by unread messages and finally by + // timestamp. + if (mentionsA && mentionsB) return timestampDiff; + } + + if (pinUnread) { + const unreadA = this._getRoomState(roomA, "unread"); + const unreadB = this._getRoomState(roomB, "unread"); + if (unreadA && !unreadB) return -1; + if (!unreadA && unreadB) return 1; + + // If they both have unread messages, sort by timestamp + // If nether have unread message (the fourth check not shown + // here), then just sort by timestamp anyways. + if (unreadA && unreadB) return timestampDiff; + } + + return timestampDiff; } _lexicographicalComparator(roomA, roomB) {