mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge remote-tracking branch 'origin/develop' into jryans/upgrade-sanitize-types
						commit
						49f1dd37f2
					
				|  | @ -58,7 +58,7 @@ | |||
|     "blueimp-canvas-to-blob": "^3.28.0", | ||||
|     "browser-encrypt-attachment": "^0.3.0", | ||||
|     "browser-request": "^0.3.3", | ||||
|     "cheerio": "^1.0.0-rc.5", | ||||
|     "cheerio": "^1.0.0-rc.9", | ||||
|     "classnames": "^2.2.6", | ||||
|     "commonmark": "^0.29.3", | ||||
|     "counterpart": "^0.18.6", | ||||
|  |  | |||
|  | @ -422,8 +422,12 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts | |||
|             safeBody = sanitizeHtml(formattedBody, sanitizeParams); | ||||
| 
 | ||||
|             if (SettingsStore.getValue("feature_latex_maths")) { | ||||
|                 const phtml = cheerio.load(safeBody, | ||||
|                     { _useHtmlParser2: true, decodeEntities: false }) | ||||
|                 const phtml = cheerio.load(safeBody, { | ||||
|                     // @ts-ignore: The `_useHtmlParser2` internal option is the
 | ||||
|                     // simplest way to both parse and render using `htmlparser2`.
 | ||||
|                     _useHtmlParser2: true, | ||||
|                     decodeEntities: false, | ||||
|                 }); | ||||
|                 // @ts-ignore - The types for `replaceWith` wrongly expect
 | ||||
|                 // Cheerio instance to be returned.
 | ||||
|                 phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) { | ||||
|  | @ -431,6 +435,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts | |||
|                         AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')), | ||||
|                         { | ||||
|                             throwOnError: false, | ||||
|                             // @ts-ignore - `e` can be an Element, not just a Node
 | ||||
|                             displayMode: e.name == 'div', | ||||
|                             output: "htmlAndMathml", | ||||
|                         }); | ||||
|  |  | |||
|  | @ -544,11 +544,13 @@ export default class MessagePanel extends React.Component { | |||
|             } | ||||
|             if (!grouper) { | ||||
|                 const wantTile = this._shouldShowEvent(mxEv); | ||||
|                 const isGrouped = false; | ||||
|                 if (wantTile) { | ||||
|                     // make sure we unpack the array returned by _getTilesForEvent,
 | ||||
|                     // otherwise react will auto-generate keys and we will end up
 | ||||
|                     // replacing all of the DOM elements every time we paginate.
 | ||||
|                     ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile)); | ||||
|                     ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped, | ||||
|                         nextEvent, nextTile)); | ||||
|                     prevEvent = mxEv; | ||||
|                 } | ||||
| 
 | ||||
|  | @ -564,7 +566,7 @@ export default class MessagePanel extends React.Component { | |||
|         return ret; | ||||
|     } | ||||
| 
 | ||||
|     _getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) { | ||||
|     _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) { | ||||
|         const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary'); | ||||
|         const EventTile = sdk.getComponent('rooms.EventTile'); | ||||
|         const DateSeparator = sdk.getComponent('messages.DateSeparator'); | ||||
|  | @ -584,7 +586,7 @@ export default class MessagePanel extends React.Component { | |||
| 
 | ||||
|         // do we need a date separator since the last event?
 | ||||
|         const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate); | ||||
|         if (wantsDateSeparator) { | ||||
|         if (wantsDateSeparator && !isGrouped) { | ||||
|             const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>; | ||||
|             ret.push(dateSeparator); | ||||
|         } | ||||
|  | @ -968,9 +970,9 @@ class CreationGrouper { | |||
| 
 | ||||
|         const DateSeparator = sdk.getComponent('messages.DateSeparator'); | ||||
|         const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); | ||||
| 
 | ||||
|         const panel = this.panel; | ||||
|         const ret = []; | ||||
|         const isGrouped = true; | ||||
|         const createEvent = this.createEvent; | ||||
|         const lastShownEvent = this.lastShownEvent; | ||||
| 
 | ||||
|  | @ -984,12 +986,12 @@ class CreationGrouper { | |||
|         // If this m.room.create event should be shown (room upgrade) then show it before the summary
 | ||||
|         if (panel._shouldShowEvent(createEvent)) { | ||||
|             // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
 | ||||
|             ret.push(...panel._getTilesForEvent(createEvent, createEvent, false)); | ||||
|             ret.push(...panel._getTilesForEvent(createEvent, createEvent)); | ||||
|         } | ||||
| 
 | ||||
|         for (const ejected of this.ejectedEvents) { | ||||
|             ret.push(...panel._getTilesForEvent( | ||||
|                 createEvent, ejected, createEvent === lastShownEvent, | ||||
|                 createEvent, ejected, createEvent === lastShownEvent, isGrouped, | ||||
|             )); | ||||
|         } | ||||
| 
 | ||||
|  | @ -998,7 +1000,7 @@ class CreationGrouper { | |||
|             // of EventListSummary, render each member event as if the previous
 | ||||
|             // one was itself. This way, the timestamp of the previous event === the
 | ||||
|             // timestamp of the current event, and no DateSeparator is inserted.
 | ||||
|             return panel._getTilesForEvent(e, e, e === lastShownEvent); | ||||
|             return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped); | ||||
|         }).reduce((a, b) => a.concat(b), []); | ||||
|         // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
 | ||||
|         const ev = this.events[this.events.length - 1]; | ||||
|  | @ -1083,7 +1085,7 @@ class RedactionGrouper { | |||
| 
 | ||||
|         const DateSeparator = sdk.getComponent('messages.DateSeparator'); | ||||
|         const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); | ||||
| 
 | ||||
|         const isGrouped = true; | ||||
|         const panel = this.panel; | ||||
|         const ret = []; | ||||
|         const lastShownEvent = this.lastShownEvent; | ||||
|  | @ -1103,7 +1105,8 @@ class RedactionGrouper { | |||
|         let eventTiles = this.events.map((e, i) => { | ||||
|             senders.add(e.sender); | ||||
|             const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1]; | ||||
|             return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile); | ||||
|             return panel._getTilesForEvent( | ||||
|                 prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile); | ||||
|         }).reduce((a, b) => a.concat(b), []); | ||||
| 
 | ||||
|         if (eventTiles.length === 0) { | ||||
|  | @ -1182,7 +1185,7 @@ class MemberGrouper { | |||
| 
 | ||||
|         const DateSeparator = sdk.getComponent('messages.DateSeparator'); | ||||
|         const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); | ||||
| 
 | ||||
|         const isGrouped = true; | ||||
|         const panel = this.panel; | ||||
|         const lastShownEvent = this.lastShownEvent; | ||||
|         const ret = []; | ||||
|  | @ -1215,7 +1218,7 @@ class MemberGrouper { | |||
|             // of MemberEventListSummary, render each member event as if the previous
 | ||||
|             // one was itself. This way, the timestamp of the previous event === the
 | ||||
|             // timestamp of the current event, and no DateSeparator is inserted.
 | ||||
|             return panel._getTilesForEvent(e, e, e === lastShownEvent); | ||||
|             return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped); | ||||
|         }).reduce((a, b) => a.concat(b), []); | ||||
| 
 | ||||
|         if (eventTiles.length === 0) { | ||||
|  |  | |||
|  | @ -27,8 +27,8 @@ import { Action } from "../../dispatcher/actions"; | |||
| import RoomListStore from "../../stores/room-list/RoomListStore"; | ||||
| import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; | ||||
| import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; | ||||
| import {replaceableComponent} from "../../utils/replaceableComponent"; | ||||
| import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore"; | ||||
| import { replaceableComponent } from "../../utils/replaceableComponent"; | ||||
| import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     isMinimized: boolean; | ||||
|  |  | |||
|  | @ -187,9 +187,15 @@ function DeviceItem({userId, device}: {userId: string, device: IDevice}) { | |||
|         verifyDevice(cli.getUser(userId), device); | ||||
|     }; | ||||
| 
 | ||||
|     const deviceName = device.ambiguous ? | ||||
|         (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : | ||||
|         device.getDisplayName(); | ||||
|     let deviceName; | ||||
|     if (!device.getDisplayName()?.trim()) { | ||||
|         deviceName = device.deviceId; | ||||
|     } else { | ||||
|         deviceName = device.ambiguous ? | ||||
|             device.getDisplayName() + " (" + device.deviceId + ")" : | ||||
|             device.getDisplayName(); | ||||
|     } | ||||
| 
 | ||||
|     let trustedLabel = null; | ||||
|     if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ limitations under the License. | |||
| */ | ||||
| import React from 'react'; | ||||
| import * as sdk from '../../../index'; | ||||
| import {_t} from '../../../languageHandler'; | ||||
| import {_t, _td} from '../../../languageHandler'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import dis from '../../../dispatcher/dispatcher'; | ||||
| import EditorModel from '../../../editor/model'; | ||||
|  | @ -24,16 +24,18 @@ import {getCaretOffsetAndText} from '../../../editor/dom'; | |||
| import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; | ||||
| import {findEditableEvent} from '../../../utils/EventUtils'; | ||||
| import {parseEvent} from '../../../editor/deserialize'; | ||||
| import {PartCreator} from '../../../editor/parts'; | ||||
| import {CommandPartCreator} from '../../../editor/parts'; | ||||
| import EditorStateTransfer from '../../../utils/EditorStateTransfer'; | ||||
| import classNames from 'classnames'; | ||||
| import {EventStatus} from 'matrix-js-sdk/src/models/event'; | ||||
| import BasicMessageComposer from "./BasicMessageComposer"; | ||||
| import MatrixClientContext from "../../../contexts/MatrixClientContext"; | ||||
| import {CommandCategories, getCommand} from '../../../SlashCommands'; | ||||
| import {Action} from "../../../dispatcher/actions"; | ||||
| import CountlyAnalytics from "../../../CountlyAnalytics"; | ||||
| import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; | ||||
| import {replaceableComponent} from "../../../utils/replaceableComponent"; | ||||
| import Modal from '../../../Modal'; | ||||
| 
 | ||||
| function _isReply(mxEvent) { | ||||
|     const relatesTo = mxEvent.getContent()["m.relates_to"]; | ||||
|  | @ -178,6 +180,22 @@ export default class EditMessageComposer extends React.Component { | |||
|         dis.fire(Action.FocusComposer); | ||||
|     } | ||||
| 
 | ||||
|     _isSlashCommand() { | ||||
|         const parts = this.model.parts; | ||||
|         const firstPart = parts[0]; | ||||
|         if (firstPart) { | ||||
|             if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") | ||||
|                 && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     _isContentModified(newContent) { | ||||
|         // if nothing has changed then bail
 | ||||
|         const oldContent = this.props.editState.getEvent().getContent(); | ||||
|  | @ -190,19 +208,112 @@ export default class EditMessageComposer extends React.Component { | |||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     _sendEdit = () => { | ||||
|     _getSlashCommand() { | ||||
|         const commandText = this.model.parts.reduce((text, part) => { | ||||
|             // use mxid to textify user pills in a command
 | ||||
|             if (part.type === "user-pill") { | ||||
|                 return text + part.resourceId; | ||||
|             } | ||||
|             return text + part.text; | ||||
|         }, ""); | ||||
|         const {cmd, args} = getCommand(commandText); | ||||
|         return [cmd, args, commandText]; | ||||
|     } | ||||
| 
 | ||||
|     async _runSlashCommand(cmd, args, roomId) { | ||||
|         const result = cmd.run(roomId, args); | ||||
|         let messageContent; | ||||
|         let error = result.error; | ||||
|         if (result.promise) { | ||||
|             try { | ||||
|                 if (cmd.category === CommandCategories.messages) { | ||||
|                     messageContent = await result.promise; | ||||
|                 } else { | ||||
|                     await result.promise; | ||||
|                 } | ||||
|             } catch (err) { | ||||
|                 error = err; | ||||
|             } | ||||
|         } | ||||
|         if (error) { | ||||
|             console.error("Command failure: %s", error); | ||||
|             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); | ||||
|             // assume the error is a server error when the command is async
 | ||||
|             const isServerError = !!result.promise; | ||||
|             const title = isServerError ? _td("Server error") : _td("Command error"); | ||||
| 
 | ||||
|             let errText; | ||||
|             if (typeof error === 'string') { | ||||
|                 errText = error; | ||||
|             } else if (error.message) { | ||||
|                 errText = error.message; | ||||
|             } else { | ||||
|                 errText = _t("Server unavailable, overloaded, or something else went wrong."); | ||||
|             } | ||||
| 
 | ||||
|             Modal.createTrackedDialog(title, '', ErrorDialog, { | ||||
|                 title: _t(title), | ||||
|                 description: errText, | ||||
|             }); | ||||
|         } else { | ||||
|             console.log("Command success."); | ||||
|             if (messageContent) return messageContent; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _sendEdit = async () => { | ||||
|         const startTime = CountlyAnalytics.getTimestamp(); | ||||
|         const editedEvent = this.props.editState.getEvent(); | ||||
|         const editContent = createEditContent(this.model, editedEvent); | ||||
|         const newContent = editContent["m.new_content"]; | ||||
|         let shouldSend = true; | ||||
| 
 | ||||
|         // If content is modified then send an updated event into the room
 | ||||
|         if (this._isContentModified(newContent)) { | ||||
|             const roomId = editedEvent.getRoomId(); | ||||
|             this._cancelPreviousPendingEdit(); | ||||
|             const prom = this.context.sendMessage(roomId, editContent); | ||||
|             dis.dispatch({action: "message_sent"}); | ||||
|             CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); | ||||
|             if (!containsEmote(this.model) && this._isSlashCommand()) { | ||||
|                 const [cmd, args, commandText] = this._getSlashCommand(); | ||||
|                 if (cmd) { | ||||
|                     if (cmd.category === CommandCategories.messages) { | ||||
|                         editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId); | ||||
|                     } else { | ||||
|                         this._runSlashCommand(cmd, args, roomId); | ||||
|                         shouldSend = false; | ||||
|                     } | ||||
|                 } else { | ||||
|                     // ask the user if their unknown command should be sent as a message
 | ||||
|                     const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); | ||||
|                     const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { | ||||
|                         title: _t("Unknown Command"), | ||||
|                         description: <div> | ||||
|                             <p> | ||||
|                                 { _t("Unrecognised command: %(commandText)s", {commandText}) } | ||||
|                             </p> | ||||
|                             <p> | ||||
|                                 { _t("You can use <code>/help</code> to list available commands. " + | ||||
|                                     "Did you mean to send this as a message?", {}, { | ||||
|                                     code: t => <code>{ t }</code>, | ||||
|                                 }) } | ||||
|                             </p> | ||||
|                             <p> | ||||
|                                 { _t("Hint: Begin your message with <code>//</code> to start it with a slash.", {}, { | ||||
|                                     code: t => <code>{ t }</code>, | ||||
|                                 }) } | ||||
|                             </p> | ||||
|                         </div>, | ||||
|                         button: _t('Send as message'), | ||||
|                     }); | ||||
|                     const [sendAnyway] = await finished; | ||||
|                     // if !sendAnyway bail to let the user edit the composer and try again
 | ||||
|                     if (!sendAnyway) return; | ||||
|                 } | ||||
|             } | ||||
|             if (shouldSend) { | ||||
|                 this._cancelPreviousPendingEdit(); | ||||
|                 const prom = this.context.sendMessage(roomId, editContent); | ||||
|                 dis.dispatch({action: "message_sent"}); | ||||
|                 CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // close the event editing and focus composer
 | ||||
|  | @ -240,7 +351,7 @@ export default class EditMessageComposer extends React.Component { | |||
|     _createEditorModel() { | ||||
|         const {editState} = this.props; | ||||
|         const room = this._getRoom(); | ||||
|         const partCreator = new PartCreator(room, this.context); | ||||
|         const partCreator = new CommandPartCreator(room, this.context); | ||||
|         let parts; | ||||
|         if (editState.hasEditorState()) { | ||||
|             // if restoring state from a previous editor,
 | ||||
|  |  | |||
|  | @ -26,13 +26,11 @@ import {SpaceItem} from "./SpaceTreeLevel"; | |||
| import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; | ||||
| import {useEventEmitter} from "../../../hooks/useEventEmitter"; | ||||
| import SpaceStore, { | ||||
|     HOME_SPACE, | ||||
|     UPDATE_INVITED_SPACES, | ||||
|     UPDATE_SELECTED_SPACE, | ||||
|     UPDATE_TOP_LEVEL_SPACES, | ||||
| } from "../../../stores/SpaceStore"; | ||||
| import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; | ||||
| import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState"; | ||||
| import NotificationBadge from "../rooms/NotificationBadge"; | ||||
| import { | ||||
|     RovingAccessibleButton, | ||||
|  | @ -40,13 +38,15 @@ import { | |||
|     RovingTabIndexProvider, | ||||
| } from "../../../accessibility/RovingTabIndex"; | ||||
| import {Key} from "../../../Keyboard"; | ||||
| import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore"; | ||||
| import {NotificationState} from "../../../stores/notifications/NotificationState"; | ||||
| 
 | ||||
| interface IButtonProps { | ||||
|     space?: Room; | ||||
|     className?: string; | ||||
|     selected?: boolean; | ||||
|     tooltip?: string; | ||||
|     notificationState?: SpaceNotificationState; | ||||
|     notificationState?: NotificationState; | ||||
|     isNarrow?: boolean; | ||||
|     onClick(): void; | ||||
| } | ||||
|  | @ -212,8 +212,8 @@ const SpacePanel = () => { | |||
|                             className="mx_SpaceButton_home" | ||||
|                             onClick={() => SpaceStore.instance.setActiveSpace(null)} | ||||
|                             selected={!activeSpace} | ||||
|                             tooltip={_t("Home")} | ||||
|                             notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)} | ||||
|                             tooltip={_t("All rooms")} | ||||
|                             notificationState={RoomNotificationStateStore.instance.globalState} | ||||
|                             isNarrow={isPanelCollapsed} | ||||
|                         /> | ||||
|                         { invites.map(s => <SpaceItem | ||||
|  |  | |||
|  | @ -116,14 +116,22 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = | |||
|     const parser = new Markdown(md); | ||||
|     if (!parser.isPlainText() || forceHTML) { | ||||
|         // feed Markdown output to HTML parser
 | ||||
|         const phtml = cheerio.load(parser.toHTML(), | ||||
|             { _useHtmlParser2: true, decodeEntities: false }); | ||||
|         const phtml = cheerio.load(parser.toHTML(), { | ||||
|             // @ts-ignore: The `_useHtmlParser2` internal option is the
 | ||||
|             // simplest way to both parse and render using `htmlparser2`.
 | ||||
|             _useHtmlParser2: true, | ||||
|             decodeEntities: false, | ||||
|         }); | ||||
| 
 | ||||
|         if (SettingsStore.getValue("feature_latex_maths")) { | ||||
|             // original Markdown without LaTeX replacements
 | ||||
|             const parserOrig = new Markdown(orig); | ||||
|             const phtmlOrig = cheerio.load(parserOrig.toHTML(), | ||||
|                 { _useHtmlParser2: true, decodeEntities: false }); | ||||
|             const phtmlOrig = cheerio.load(parserOrig.toHTML(), { | ||||
|                 // @ts-ignore: The `_useHtmlParser2` internal option is the
 | ||||
|                 // simplest way to both parse and render using `htmlparser2`.
 | ||||
|                 _useHtmlParser2: true, | ||||
|                 decodeEntities: false, | ||||
|             }); | ||||
| 
 | ||||
|             // since maths delimiters are handled before Markdown,
 | ||||
|             // code blocks could contain mangled content.
 | ||||
|  |  | |||
|  | @ -1012,7 +1012,7 @@ | |||
|     "Create": "Create", | ||||
|     "Expand space panel": "Expand space panel", | ||||
|     "Collapse space panel": "Collapse space panel", | ||||
|     "Home": "Home", | ||||
|     "All rooms": "All rooms", | ||||
|     "Click to copy": "Click to copy", | ||||
|     "Copied!": "Copied!", | ||||
|     "Failed to copy": "Failed to copy", | ||||
|  | @ -1441,6 +1441,13 @@ | |||
|     "Someone is using an unknown session": "Someone is using an unknown session", | ||||
|     "This room is end-to-end encrypted": "This room is end-to-end encrypted", | ||||
|     "Everyone in this room is verified": "Everyone in this room is verified", | ||||
|     "Server error": "Server error", | ||||
|     "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", | ||||
|     "Unknown Command": "Unknown Command", | ||||
|     "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", | ||||
|     "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?", | ||||
|     "Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.", | ||||
|     "Send as message": "Send as message", | ||||
|     "Edit message": "Edit message", | ||||
|     "Mod": "Mod", | ||||
|     "This event could not be displayed": "This event could not be displayed", | ||||
|  | @ -1631,13 +1638,6 @@ | |||
|     "This Room": "This Room", | ||||
|     "All Rooms": "All Rooms", | ||||
|     "Search…": "Search…", | ||||
|     "Server error": "Server error", | ||||
|     "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", | ||||
|     "Unknown Command": "Unknown Command", | ||||
|     "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", | ||||
|     "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?", | ||||
|     "Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.", | ||||
|     "Send as message": "Send as message", | ||||
|     "Failed to connect to integration manager": "Failed to connect to integration manager", | ||||
|     "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", | ||||
|     "Add some now": "Add some now", | ||||
|  | @ -2016,10 +2016,10 @@ | |||
|     "Continue with %(provider)s": "Continue with %(provider)s", | ||||
|     "Sign in with single sign-on": "Sign in with single sign-on", | ||||
|     "And %(count)s more...|other": "And %(count)s more...", | ||||
|     "Home": "Home", | ||||
|     "Enter a server name": "Enter a server name", | ||||
|     "Looks good": "Looks good", | ||||
|     "Can't find this server or its room list": "Can't find this server or its room list", | ||||
|     "All rooms": "All rooms", | ||||
|     "Your server": "Your server", | ||||
|     "Are you sure you want to remove <b>%(serverName)s</b>": "Are you sure you want to remove <b>%(serverName)s</b>", | ||||
|     "Remove server": "Remove server", | ||||
|  |  | |||
|  | @ -31,28 +31,23 @@ import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateS | |||
| import {DefaultTagID} from "./room-list/models"; | ||||
| import {EnhancedMap, mapDiff} from "../utils/maps"; | ||||
| import {setHasDiff} from "../utils/sets"; | ||||
| import {objectDiff} from "../utils/objects"; | ||||
| import {arrayHasDiff} from "../utils/arrays"; | ||||
| import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; | ||||
| import RoomViewStore from "./RoomViewStore"; | ||||
| 
 | ||||
| type SpaceKey = string | symbol; | ||||
| 
 | ||||
| interface IState {} | ||||
| 
 | ||||
| const ACTIVE_SPACE_LS_KEY = "mx_active_space"; | ||||
| 
 | ||||
| export const HOME_SPACE = Symbol("home-space"); | ||||
| export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); | ||||
| 
 | ||||
| export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); | ||||
| export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); | ||||
| export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); | ||||
| // Space Room ID/HOME_SPACE will be emitted when a Space's children change
 | ||||
| // Space Room ID will be emitted when a Space's children change
 | ||||
| 
 | ||||
| const MAX_SUGGESTED_ROOMS = 20; | ||||
| 
 | ||||
| const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "home_space"}`; | ||||
| const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`; | ||||
| 
 | ||||
| const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
 | ||||
|     return arr.reduce((result, room: Room) => { | ||||
|  | @ -86,15 +81,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
| 
 | ||||
|     // The spaces representing the roots of the various tree-like hierarchies
 | ||||
|     private rootSpaces: Room[] = []; | ||||
|     // The list of rooms not present in any currently joined spaces
 | ||||
|     private orphanedRooms = new Set<string>(); | ||||
|     // Map from room ID to set of spaces which list it as a child
 | ||||
|     private parentMap = new EnhancedMap<string, Set<string>>(); | ||||
|     // Map from space key to SpaceNotificationState instance representing that space
 | ||||
|     private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>(); | ||||
|     // Map from spaceId to SpaceNotificationState instance representing that space
 | ||||
|     private notificationStateMap = new Map<string, SpaceNotificationState>(); | ||||
|     // Map from space key to Set of room IDs that should be shown as part of that space's filter
 | ||||
|     private spaceFilteredRooms = new Map<string | symbol, Set<string>>(); | ||||
|     // The space currently selected in the Space Panel - if null then `Home` is selected
 | ||||
|     private spaceFilteredRooms = new Map<string, Set<string>>(); | ||||
|     // The space currently selected in the Space Panel - if null then All Rooms is selected
 | ||||
|     private _activeSpace?: Room = null; | ||||
|     private _suggestedRooms: ISpaceSummaryRoom[] = []; | ||||
|     private _invitedSpaces = new Set<Room>(); | ||||
|  | @ -244,7 +237,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|     } | ||||
| 
 | ||||
|     public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => { | ||||
|         return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); | ||||
|         if (!space) { | ||||
|             return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); | ||||
|         } | ||||
|         return this.spaceFilteredRooms.get(space.roomId) || new Set(); | ||||
|     }; | ||||
| 
 | ||||
|     private rebuild = throttle(() => { | ||||
|  | @ -275,7 +271,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); | ||||
|         const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren)); | ||||
| 
 | ||||
|         // somewhat algorithm to handle full-cycles
 | ||||
|         const detachedNodes = new Set<Room>(spaces); | ||||
|  | @ -316,7 +312,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|         //     rootSpaces.push(space);
 | ||||
|         // });
 | ||||
| 
 | ||||
|         this.orphanedRooms = new Set(orphanedRooms); | ||||
|         this.rootSpaces = rootSpaces; | ||||
|         this.parentMap = backrefs; | ||||
| 
 | ||||
|  | @ -337,25 +332,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|         this.rebuild(); | ||||
|     } | ||||
| 
 | ||||
|     private showInHomeSpace = (room: Room) => { | ||||
|         if (room.isSpaceRoom()) return false; | ||||
|         return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
 | ||||
|             || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
 | ||||
|             || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
 | ||||
|     }; | ||||
| 
 | ||||
|     // Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
 | ||||
|     // This can only change whether it shows up in the HOME_SPACE or not
 | ||||
|     private onRoomUpdate = (room: Room) => { | ||||
|         if (this.showInHomeSpace(room)) { | ||||
|             this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId); | ||||
|             this.emit(HOME_SPACE); | ||||
|         } else if (!this.orphanedRooms.has(room.roomId)) { | ||||
|             this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId); | ||||
|             this.emit(HOME_SPACE); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     private onSpaceMembersChange = (ev: MatrixEvent) => { | ||||
|         // skip this update if we do not have a DM with this user
 | ||||
|         if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return; | ||||
|  | @ -369,16 +345,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|         const oldFilteredRooms = this.spaceFilteredRooms; | ||||
|         this.spaceFilteredRooms = new Map(); | ||||
| 
 | ||||
|         // put all room invites in the Home Space
 | ||||
|         const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); | ||||
|         this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId))); | ||||
| 
 | ||||
|         visibleRooms.forEach(room => { | ||||
|             if (this.showInHomeSpace(room)) { | ||||
|                 this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         this.rootSpaces.forEach(s => { | ||||
|             // traverse each space tree in DFS to build up the supersets as you go up,
 | ||||
|             // reusing results from like subtrees.
 | ||||
|  | @ -425,13 +391,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|             // Update NotificationStates
 | ||||
|             this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { | ||||
|                 if (roomIds.has(room.roomId)) { | ||||
|                     // Don't aggregate notifications for DMs except in the Home Space
 | ||||
|                     if (s !== HOME_SPACE) { | ||||
|                         return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) | ||||
|                             || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); | ||||
|                     } | ||||
| 
 | ||||
|                     return true; | ||||
|                     return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) | ||||
|                         || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); | ||||
|                 } | ||||
| 
 | ||||
|                 return false; | ||||
|  | @ -513,8 +474,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|                 // TODO confirm this after implementing parenting behaviour
 | ||||
|                 if (room.isSpaceRoom()) { | ||||
|                     this.onSpaceUpdate(); | ||||
|                 } else { | ||||
|                     this.onRoomUpdate(room); | ||||
|                 } | ||||
|                 this.emit(room.roomId); | ||||
|                 break; | ||||
|  | @ -527,38 +486,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => { | ||||
|         if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { | ||||
|             // If the room was in favourites and now isn't or the opposite then update its position in the trees
 | ||||
|             const oldTags = lastEvent?.getContent()?.tags || {}; | ||||
|             const newTags = ev.getContent()?.tags || {}; | ||||
|             if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { | ||||
|                 this.onRoomUpdate(room); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => { | ||||
|         if (ev.getType() === EventType.Direct) { | ||||
|             const lastContent = lastEvent.getContent(); | ||||
|             const content = ev.getContent(); | ||||
| 
 | ||||
|             const diff = objectDiff<Record<string, string[]>>(lastContent, content); | ||||
|             // filter out keys which changed by reference only by checking whether the sets differ
 | ||||
|             const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k])); | ||||
|             // DM tag changes, refresh relevant rooms
 | ||||
|             new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => { | ||||
|                 const room = this.matrixClient?.getRoom(roomId); | ||||
|                 if (room) { | ||||
|                     this.onRoomUpdate(room); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     protected async reset() { | ||||
|         this.rootSpaces = []; | ||||
|         this.orphanedRooms = new Set(); | ||||
|         this.parentMap = new EnhancedMap(); | ||||
|         this.notificationStateMap = new Map(); | ||||
|         this.spaceFilteredRooms = new Map(); | ||||
|  | @ -573,8 +502,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|             this.matrixClient.removeListener("Room", this.onRoom); | ||||
|             this.matrixClient.removeListener("Room.myMembership", this.onRoom); | ||||
|             this.matrixClient.removeListener("RoomState.events", this.onRoomState); | ||||
|             this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); | ||||
|             this.matrixClient.removeListener("accountData", this.onAccountData); | ||||
|         } | ||||
|         await this.reset(); | ||||
|     } | ||||
|  | @ -584,8 +511,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|         this.matrixClient.on("Room", this.onRoom); | ||||
|         this.matrixClient.on("Room.myMembership", this.onRoom); | ||||
|         this.matrixClient.on("RoomState.events", this.onRoomState); | ||||
|         this.matrixClient.on("Room.accountData", this.onRoomAccountData); | ||||
|         this.matrixClient.on("accountData", this.onAccountData); | ||||
| 
 | ||||
|         await this.onSpaceUpdate(); // trigger an initial update
 | ||||
| 
 | ||||
|  | @ -610,7 +535,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|                     // Don't context switch when navigating to the space room
 | ||||
|                     // as it will cause you to end up in the wrong room
 | ||||
|                     this.setActiveSpace(room, false); | ||||
|                 } else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { | ||||
|                 } else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { | ||||
|                     this.switchToRelatedSpace(roomId); | ||||
|                 } | ||||
| 
 | ||||
|  | @ -628,7 +553,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public getNotificationState(key: SpaceKey): SpaceNotificationState { | ||||
|     public getNotificationState(key: string): SpaceNotificationState { | ||||
|         if (this.notificationStateMap.has(key)) { | ||||
|             return this.notificationStateMap.get(key); | ||||
|         } | ||||
|  |  | |||
|  | @ -680,7 +680,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> { | |||
|             promise = this.recalculatePrefiltering(); | ||||
|         } else { | ||||
|             this.filterConditions.push(filter); | ||||
|             // Runtime filters with spaces disable prefiltering for the search all spaces effect
 | ||||
|             // Runtime filters with spaces disable prefiltering for the search all spaces feature
 | ||||
|             if (SettingsStore.getValue("feature_spaces")) { | ||||
|                 // this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below
 | ||||
|                 // this way the runtime filters are only evaluated on one dataset and not both.
 | ||||
|  | @ -712,10 +712,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> { | |||
| 
 | ||||
|             if (this.algorithm) { | ||||
|                 this.algorithm.removeFilterCondition(filter); | ||||
|                 // Runtime filters with spaces disable prefiltering for the search all spaces effect
 | ||||
|                 if (SettingsStore.getValue("feature_spaces")) { | ||||
|                     promise = this.recalculatePrefiltering(); | ||||
|                 } | ||||
|             } | ||||
|             // Runtime filters with spaces disable prefiltering for the search all spaces feature
 | ||||
|             if (SettingsStore.getValue("feature_spaces")) { | ||||
|                 promise = this.recalculatePrefiltering(); | ||||
|             } | ||||
|         } | ||||
|         idx = this.prefilterConditions.indexOf(filter); | ||||
|  |  | |||
|  | @ -24,26 +24,34 @@ import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore"; | |||
|  * Watches for changes in spaces to manage the filter on the provided RoomListStore | ||||
|  */ | ||||
| export class SpaceWatcher { | ||||
|     private filter = new SpaceFilterCondition(); | ||||
|     private filter: SpaceFilterCondition; | ||||
|     private activeSpace: Room = SpaceStore.instance.activeSpace; | ||||
| 
 | ||||
|     constructor(private store: RoomListStoreClass) { | ||||
|         this.updateFilter(); // get the filter into a consistent state
 | ||||
|         store.addFilter(this.filter); | ||||
|         SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated); | ||||
|     } | ||||
| 
 | ||||
|     private onSelectedSpaceUpdated = (activeSpace: Room) => { | ||||
|     private onSelectedSpaceUpdated = (activeSpace?: Room) => { | ||||
|         this.activeSpace = activeSpace; | ||||
|         this.updateFilter(); | ||||
| 
 | ||||
|         if (this.filter) { | ||||
|             if (activeSpace) { | ||||
|                 this.updateFilter(); | ||||
|             } else { | ||||
|                 this.store.removeFilter(this.filter); | ||||
|                 this.filter = null; | ||||
|             } | ||||
|         } else if (activeSpace) { | ||||
|             this.filter = new SpaceFilterCondition(); | ||||
|             this.updateFilter(); | ||||
|             this.store.addFilter(this.filter); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     private updateFilter = () => { | ||||
|         if (this.activeSpace) { | ||||
|             SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { | ||||
|                 this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); | ||||
|             }); | ||||
|         } | ||||
|         SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { | ||||
|             this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); | ||||
|         }); | ||||
|         this.filter.updateSpace(this.activeSpace); | ||||
|     }; | ||||
| } | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; | |||
| 
 | ||||
| import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; | ||||
| import { IDestroyable } from "../../../utils/IDestroyable"; | ||||
| import SpaceStore, {HOME_SPACE} from "../../SpaceStore"; | ||||
| import SpaceStore from "../../SpaceStore"; | ||||
| import { setHasDiff } from "../../../utils/sets"; | ||||
| 
 | ||||
| /** | ||||
|  | @ -55,10 +55,12 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE; | ||||
|     private getSpaceEventKey = (space: Room) => space.roomId; | ||||
| 
 | ||||
|     public updateSpace(space: Room) { | ||||
|         SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); | ||||
|         if (this.space) { | ||||
|             SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); | ||||
|         } | ||||
|         SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate); | ||||
|         this.onStoreUpdate(); // initial update from the change to the space
 | ||||
|     } | ||||
|  |  | |||
|  | @ -77,7 +77,7 @@ describe('MessagePanel', function() { | |||
|         DMRoomMap.makeShared(); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(function() { | ||||
|     afterEach(function () { | ||||
|         clock.uninstall(); | ||||
|     }); | ||||
| 
 | ||||
|  | @ -88,7 +88,21 @@ describe('MessagePanel', function() { | |||
|             events.push(test_utils.mkMessage( | ||||
|                 { | ||||
|                     event: true, room: "!room:id", user: "@user:id", | ||||
|                     ts: ts0 + i*1000, | ||||
|                     ts: ts0 + i * 1000, | ||||
|                 })); | ||||
|         } | ||||
|         return events; | ||||
|     } | ||||
| 
 | ||||
|     // Just to avoid breaking Dateseparator tests that might run at 00hrs
 | ||||
|     function mkOneDayEvents() { | ||||
|         const events = []; | ||||
|         const ts0 = Date.parse('09 May 2004 00:12:00 GMT'); | ||||
|         for (let i = 0; i < 10; i++) { | ||||
|             events.push(test_utils.mkMessage( | ||||
|                 { | ||||
|                     event: true, room: "!room:id", user: "@user:id", | ||||
|                     ts: ts0 + i * 1000, | ||||
|                 })); | ||||
|         } | ||||
|         return events; | ||||
|  | @ -104,7 +118,7 @@ describe('MessagePanel', function() { | |||
|         let i = 0; | ||||
|         events.push(test_utils.mkMessage({ | ||||
|             event: true, room: "!room:id", user: "@user:id", | ||||
|             ts: ts0 + ++i*1000, | ||||
|             ts: ts0 + ++i * 1000, | ||||
|         })); | ||||
| 
 | ||||
|         for (i = 0; i < 10; i++) { | ||||
|  | @ -151,7 +165,7 @@ describe('MessagePanel', function() { | |||
|                     }, | ||||
|                     getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', | ||||
|                 }, | ||||
|                 ts: ts0 + i*1000, | ||||
|                 ts: ts0 + i * 1000, | ||||
|                 mship: 'join', | ||||
|                 prevMship: 'join', | ||||
|                 name: 'A user', | ||||
|  | @ -250,7 +264,6 @@ describe('MessagePanel', function() { | |||
|             }), | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     function isReadMarkerVisible(rmContainer) { | ||||
|         return rmContainer && rmContainer.children.length > 0; | ||||
|     } | ||||
|  | @ -437,4 +450,17 @@ describe('MessagePanel', function() { | |||
|         // read marker should be hidden given props and at the last event
 | ||||
|         expect(isReadMarkerVisible(rm)).toBeFalsy(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should render Date separators for the events', function () { | ||||
|         const events = mkOneDayEvents(); | ||||
|         const res = mount( | ||||
|             <WrappedMessagePanel | ||||
|                 className="cls" | ||||
|                 events={events} | ||||
|             />, | ||||
|         ); | ||||
|         const Dates = res.find(sdk.getComponent('messages.DateSeparator')); | ||||
|         | ||||
|         expect(Dates.length).toEqual(1); | ||||
|     }); | ||||
| }); | ||||
|  |  | |||
|  | @ -101,6 +101,7 @@ const invite1 = "!invite1:server"; | |||
| const invite2 = "!invite2:server"; | ||||
| const room1 = "!room1:server"; | ||||
| const room2 = "!room2:server"; | ||||
| const room3 = "!room3:server"; | ||||
| const space1 = "!space1:server"; | ||||
| const space2 = "!space2:server"; | ||||
| const space3 = "!space3:server"; | ||||
|  | @ -361,8 +362,8 @@ describe("SpaceStore", () => { | |||
|                 expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy(); | ||||
|             }); | ||||
| 
 | ||||
|             it("home space does not contain rooms/low priority from rooms within spaces", () => { | ||||
|                 expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy(); | ||||
|             it("home space does contain rooms/low priority even if they are also shown in a space", () => { | ||||
|                 expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy(); | ||||
|             }); | ||||
| 
 | ||||
|             it("space contains child rooms", () => { | ||||
|  | @ -614,8 +615,8 @@ describe("SpaceStore", () => { | |||
| 
 | ||||
|     describe("space auto switching tests", () => { | ||||
|         beforeEach(async () => { | ||||
|             [room1, room2, orphan1].forEach(mkRoom); | ||||
|             mkSpace(space1, [room1, room2]); | ||||
|             [room1, room2, room3, orphan1].forEach(mkRoom); | ||||
|             mkSpace(space1, [room1, room2, room3]); | ||||
|             mkSpace(space2, [room1, room2]); | ||||
| 
 | ||||
|             client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([ | ||||
|  | @ -641,15 +642,15 @@ describe("SpaceStore", () => { | |||
| 
 | ||||
|         it("switch to canonical parent space for room", async () => { | ||||
|             viewRoom(room1); | ||||
|             await store.setActiveSpace(null, false); | ||||
|             await store.setActiveSpace(client.getRoom(space2), false); | ||||
|             viewRoom(room2); | ||||
|             expect(store.activeSpace).toBe(client.getRoom(space2)); | ||||
|         }); | ||||
| 
 | ||||
|         it("switch to first containing space for room", async () => { | ||||
|             viewRoom(room2); | ||||
|             await store.setActiveSpace(null, false); | ||||
|             viewRoom(room1); | ||||
|             await store.setActiveSpace(client.getRoom(space2), false); | ||||
|             viewRoom(room3); | ||||
|             expect(store.activeSpace).toBe(client.getRoom(space1)); | ||||
|         }); | ||||
| 
 | ||||
|  | @ -659,6 +660,13 @@ describe("SpaceStore", () => { | |||
|             viewRoom(orphan1); | ||||
|             expect(store.activeSpace).toBeNull(); | ||||
|         }); | ||||
| 
 | ||||
|         it("when switching rooms in the all rooms home space don't switch to related space", async () => { | ||||
|             viewRoom(room2); | ||||
|             await store.setActiveSpace(null, false); | ||||
|             viewRoom(room1); | ||||
|             expect(store.activeSpace).toBeNull(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     describe("traverseSpace", () => { | ||||
|  |  | |||
							
								
								
									
										127
									
								
								yarn.lock
								
								
								
								
							
							
						
						
									
										127
									
								
								yarn.lock
								
								
								
								
							|  | @ -2401,29 +2401,29 @@ chardet@^0.7.0: | |||
|   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" | ||||
|   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== | ||||
| 
 | ||||
| cheerio-select-tmp@^0.1.0: | ||||
|   version "0.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646" | ||||
|   integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ== | ||||
| cheerio-select@^1.4.0: | ||||
|   version "1.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.4.0.tgz#3a16f21e37a2ef0f211d6d1aa4eff054bb22cdc9" | ||||
|   integrity sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew== | ||||
|   dependencies: | ||||
|     css-select "^3.1.2" | ||||
|     css-what "^4.0.0" | ||||
|     domelementtype "^2.1.0" | ||||
|     domhandler "^4.0.0" | ||||
|     domutils "^2.4.4" | ||||
|     css-select "^4.1.2" | ||||
|     css-what "^5.0.0" | ||||
|     domelementtype "^2.2.0" | ||||
|     domhandler "^4.2.0" | ||||
|     domutils "^2.6.0" | ||||
| 
 | ||||
| cheerio@^1.0.0-rc.3, cheerio@^1.0.0-rc.5: | ||||
|   version "1.0.0-rc.5" | ||||
|   resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f" | ||||
|   integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw== | ||||
| cheerio@^1.0.0-rc.3, cheerio@^1.0.0-rc.9: | ||||
|   version "1.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.9.tgz#a3ae6b7ce7af80675302ff836f628e7cb786a67f" | ||||
|   integrity sha512-QF6XVdrLONO6DXRF5iaolY+odmhj2CLj+xzNod7INPWMi/x9X4SOylH0S/vaPpX+AUU6t04s34SQNh7DbkuCng== | ||||
|   dependencies: | ||||
|     cheerio-select-tmp "^0.1.0" | ||||
|     dom-serializer "~1.2.0" | ||||
|     domhandler "^4.0.0" | ||||
|     entities "~2.1.0" | ||||
|     htmlparser2 "^6.0.0" | ||||
|     parse5 "^6.0.0" | ||||
|     parse5-htmlparser2-tree-adapter "^6.0.0" | ||||
|     cheerio-select "^1.4.0" | ||||
|     dom-serializer "^1.3.1" | ||||
|     domhandler "^4.2.0" | ||||
|     htmlparser2 "^6.1.0" | ||||
|     parse5 "^6.0.1" | ||||
|     parse5-htmlparser2-tree-adapter "^6.0.1" | ||||
|     tslib "^2.2.0" | ||||
| 
 | ||||
| chokidar@^3.4.0, chokidar@^3.5.1: | ||||
|   version "3.5.1" | ||||
|  | @ -2705,21 +2705,21 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: | |||
|     shebang-command "^2.0.0" | ||||
|     which "^2.0.1" | ||||
| 
 | ||||
| css-select@^3.1.2: | ||||
|   version "3.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" | ||||
|   integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== | ||||
| css-select@^4.1.2: | ||||
|   version "4.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" | ||||
|   integrity sha512-nu5ye2Hg/4ISq4XqdLY2bEatAcLIdt3OYGFc9Tm9n7VSlFBcfRv0gBNksHRgSdUDQGtN3XrZ94ztW+NfzkFSUw== | ||||
|   dependencies: | ||||
|     boolbase "^1.0.0" | ||||
|     css-what "^4.0.0" | ||||
|     domhandler "^4.0.0" | ||||
|     domutils "^2.4.3" | ||||
|     css-what "^5.0.0" | ||||
|     domhandler "^4.2.0" | ||||
|     domutils "^2.6.0" | ||||
|     nth-check "^2.0.0" | ||||
| 
 | ||||
| css-what@^4.0.0: | ||||
|   version "4.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" | ||||
|   integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== | ||||
| css-what@^5.0.0: | ||||
|   version "5.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.0.tgz#f0bf4f8bac07582722346ab243f6a35b512cfc47" | ||||
|   integrity sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA== | ||||
| 
 | ||||
| cssesc@^3.0.0: | ||||
|   version "3.0.0" | ||||
|  | @ -2925,9 +2925,9 @@ doctrine@^3.0.0: | |||
|     esutils "^2.0.2" | ||||
| 
 | ||||
| dom-helpers@^5.0.1: | ||||
|   version "5.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" | ||||
|   integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== | ||||
|   version "5.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" | ||||
|   integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== | ||||
|   dependencies: | ||||
|     "@babel/runtime" "^7.8.7" | ||||
|     csstype "^3.0.2" | ||||
|  | @ -2940,10 +2940,10 @@ dom-serializer@0: | |||
|     domelementtype "^2.0.1" | ||||
|     entities "^2.0.0" | ||||
| 
 | ||||
| dom-serializer@^1.0.1, dom-serializer@~1.2.0: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" | ||||
|   integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== | ||||
| dom-serializer@^1.0.1, dom-serializer@^1.3.1: | ||||
|   version "1.3.1" | ||||
|   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be" | ||||
|   integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q== | ||||
|   dependencies: | ||||
|     domelementtype "^2.0.1" | ||||
|     domhandler "^4.0.0" | ||||
|  | @ -2954,10 +2954,10 @@ domelementtype@1, domelementtype@^1.3.1: | |||
|   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" | ||||
|   integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== | ||||
| 
 | ||||
| domelementtype@^2.0.1, domelementtype@^2.1.0: | ||||
|   version "2.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" | ||||
|   integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== | ||||
| domelementtype@^2.0.1, domelementtype@^2.2.0: | ||||
|   version "2.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" | ||||
|   integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== | ||||
| 
 | ||||
| domexception@^2.0.1: | ||||
|   version "2.0.1" | ||||
|  | @ -2973,12 +2973,12 @@ domhandler@^2.3.0: | |||
|   dependencies: | ||||
|     domelementtype "1" | ||||
| 
 | ||||
| domhandler@^4.0.0: | ||||
|   version "4.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" | ||||
|   integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== | ||||
| domhandler@^4.0.0, domhandler@^4.2.0: | ||||
|   version "4.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" | ||||
|   integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== | ||||
|   dependencies: | ||||
|     domelementtype "^2.1.0" | ||||
|     domelementtype "^2.2.0" | ||||
| 
 | ||||
| domutils@^1.5.1: | ||||
|   version "1.7.0" | ||||
|  | @ -2988,14 +2988,14 @@ domutils@^1.5.1: | |||
|     dom-serializer "0" | ||||
|     domelementtype "1" | ||||
| 
 | ||||
| domutils@^2.4.3, domutils@^2.4.4: | ||||
|   version "2.4.4" | ||||
|   resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" | ||||
|   integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== | ||||
| domutils@^2.4.4, domutils@^2.5.2, domutils@^2.6.0: | ||||
|   version "2.6.0" | ||||
|   resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7" | ||||
|   integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA== | ||||
|   dependencies: | ||||
|     dom-serializer "^1.0.1" | ||||
|     domelementtype "^2.0.1" | ||||
|     domhandler "^4.0.0" | ||||
|     domelementtype "^2.2.0" | ||||
|     domhandler "^4.2.0" | ||||
| 
 | ||||
| ecc-jsbn@~0.1.1: | ||||
|   version "0.1.2" | ||||
|  | @ -3061,7 +3061,7 @@ entities@^1.1.1: | |||
|   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" | ||||
|   integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== | ||||
| 
 | ||||
| entities@^2.0.0, entities@~2.1.0: | ||||
| entities@^2.0.0: | ||||
|   version "2.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" | ||||
|   integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== | ||||
|  | @ -4281,6 +4281,16 @@ htmlparser2@^6.0.0: | |||
|     domutils "^2.4.4" | ||||
|     entities "^2.0.0" | ||||
| 
 | ||||
| htmlparser2@^6.1.0: | ||||
|   version "6.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" | ||||
|   integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== | ||||
|   dependencies: | ||||
|     domelementtype "^2.0.1" | ||||
|     domhandler "^4.0.0" | ||||
|     domutils "^2.5.2" | ||||
|     entities "^2.0.0" | ||||
| 
 | ||||
| http-signature@~1.2.0: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" | ||||
|  | @ -6295,7 +6305,7 @@ parse-srcset@^1.0.2: | |||
|   resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" | ||||
|   integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= | ||||
| 
 | ||||
| parse5-htmlparser2-tree-adapter@^6.0.0: | ||||
| parse5-htmlparser2-tree-adapter@^6.0.1: | ||||
|   version "6.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" | ||||
|   integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== | ||||
|  | @ -6307,7 +6317,7 @@ parse5@5.1.1: | |||
|   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" | ||||
|   integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== | ||||
| 
 | ||||
| parse5@^6.0.0, parse5@^6.0.1: | ||||
| parse5@^6.0.1: | ||||
|   version "6.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" | ||||
|   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== | ||||
|  | @ -7988,6 +7998,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3: | |||
|   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" | ||||
|   integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== | ||||
| 
 | ||||
| tslib@^2.2.0: | ||||
|   version "2.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" | ||||
|   integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== | ||||
| 
 | ||||
| tsutils@^3.17.1: | ||||
|   version "3.19.1" | ||||
|   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.19.1.tgz#d8566e0c51c82f32f9c25a4d367cd62409a547a9" | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 J. Ryan Stinnett
						J. Ryan Stinnett