UNPKG

matrix-react-sdk

Version:
1,245 lines (1,013 loc) 154 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _reactDom = _interopRequireDefault(require("react-dom")); var _propTypes = _interopRequireDefault(require("prop-types")); var _classnames = _interopRequireDefault(require("classnames")); var _shouldHideEvent = _interopRequireDefault(require("../../shouldHideEvent")); var _DateUtils = require("../../DateUtils"); var sdk = _interopRequireWildcard(require("../../index")); var _MatrixClientPeg = require("../../MatrixClientPeg"); var _SettingsStore = _interopRequireDefault(require("../../settings/SettingsStore")); var _Layout = require("../../settings/Layout"); var _languageHandler = require("../../languageHandler"); var _EventTile = require("../views/rooms/EventTile"); var _TextForEvent = require("../../TextForEvent"); var _IRCTimelineProfileResizer = _interopRequireDefault(require("../views/elements/IRCTimelineProfileResizer")); var _DMRoomMap = _interopRequireDefault(require("../../utils/DMRoomMap")); var _NewRoomIntro = _interopRequireDefault(require("../views/rooms/NewRoomIntro")); var _replaceableComponent = require("../../utils/replaceableComponent"); var _dispatcher = _interopRequireDefault(require("../../dispatcher/dispatcher")); var _dec, _class, _class2, _temp; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; // check if there is a previous event and it has the same sender as this event // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL function shouldFormContinuation(prevEvent, mxEvent) { // sanity check inputs if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false; // check if within the max continuation period if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false; // As we summarise redactions, do not continue a redacted event onto a non-redacted one and vice-versa if (mxEvent.isRedacted() !== prevEvent.isRedacted()) return false; // Some events should appear as continuations from previous events of different types. if (mxEvent.getType() !== prevEvent.getType() && (!continuedTypes.includes(mxEvent.getType()) || !continuedTypes.includes(prevEvent.getType()))) return false; // Check if the sender is the same and hasn't changed their displayname/avatar between these events if (mxEvent.sender.userId !== prevEvent.sender.userId || mxEvent.sender.name !== prevEvent.sender.name || mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false; // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile if (!(0, _EventTile.haveTileForEvent)(prevEvent)) return false; return true; } const isMembershipChange = e => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite'; /* (almost) stateless UI component which builds the event tiles in the room timeline. */ let MessagePanel = (_dec = (0, _replaceableComponent.replaceableComponent)("structures.MessagePanel"), _dec(_class = (_temp = _class2 = class MessagePanel extends _react.default.Component { constructor(props) { super(props); (0, _defineProperty2.default)(this, "onShowTypingNotificationsChange", () => { this.setState({ showTypingNotifications: _SettingsStore.default.getValue("showTypingNotifications") }); }); (0, _defineProperty2.default)(this, "_isUnmounting", () => { return !this._isMounted; }); (0, _defineProperty2.default)(this, "_collectGhostReadMarker", node => { if (node) { // now the element has appeared, change the style which will trigger the CSS transition requestAnimationFrame(() => { node.style.width = '10%'; node.style.opacity = '0'; }); } }); (0, _defineProperty2.default)(this, "_onGhostTransitionEnd", ev => { // we can now clean up the ghost element const finishedEventId = ev.target.dataset.eventid; this.setState({ ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId) }); }); (0, _defineProperty2.default)(this, "_collectEventNode", (eventId, node) => { this.eventNodes[eventId] = node; }); (0, _defineProperty2.default)(this, "_onHeightChanged", () => { const scrollPanel = this._scrollPanel.current; if (scrollPanel) { scrollPanel.checkScroll(); } }); (0, _defineProperty2.default)(this, "_onTypingShown", () => { const scrollPanel = this._scrollPanel.current; // this will make the timeline grow, so checkScroll scrollPanel.checkScroll(); if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { scrollPanel.preventShrinking(); } }); (0, _defineProperty2.default)(this, "_onTypingHidden", () => { const scrollPanel = this._scrollPanel.current; if (scrollPanel) { // as hiding the typing notifications doesn't // update the scrollPanel, we tell it to apply // the shrinking prevention once the typing notifs are hidden scrollPanel.updatePreventShrinking(); // order is important here as checkScroll will scroll down to // reveal added padding to balance the notifs disappearing. scrollPanel.checkScroll(); } }); this.state = { // previous positions the read marker has been in, so we can // display 'ghost' read markers that are animating away ghostReadMarkers: [], showTypingNotifications: _SettingsStore.default.getValue("showTypingNotifications") }; // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations this._readReceiptMap = {}; // Track read receipts by event ID. For each _shown_ event ID, we store // the list of read receipts to display: // [ // { // userId: string, // member: RoomMember, // ts: number, // }, // ] // This is recomputed on each render. It's only stored on the component // for ease of passing the data around since it's computed in one pass // over all events. this._readReceiptsByEvent = {}; // Track read receipts by user ID. For each user ID we've ever shown a // a read receipt for, we store an object: // { // lastShownEventId: string, // receipt: { // userId: string, // member: RoomMember, // ts: number, // }, // } // so that we can always keep receipts displayed by reverting back to // the last shown event for that user ID when needed. This may feel like // it duplicates the receipt storage in the room, but at this layer, we // are tracking _shown_ event IDs, which the JS SDK knows nothing about. // This is recomputed on each render, using the data from the previous // render as our fallback for any user IDs we can't match a receipt to a // displayed event in the current render cycle. this._readReceiptsByUserId = {}; // Cache hidden events setting on mount since Settings is expensive to // query, and we check this in a hot code path. this._showHiddenEventsInTimeline = _SettingsStore.default.getValue("showHiddenEventsInTimeline"); this._isMounted = false; this._readMarkerNode = /*#__PURE__*/(0, _react.createRef)(); this._whoIsTyping = /*#__PURE__*/(0, _react.createRef)(); this._scrollPanel = /*#__PURE__*/(0, _react.createRef)(); this._showTypingNotificationsWatcherRef = _SettingsStore.default.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); } componentDidMount() { this._isMounted = true; } componentWillUnmount() { this._isMounted = false; _SettingsStore.default.unwatchSetting(this._showTypingNotificationsWatcherRef); } componentDidUpdate(prevProps, prevState) { if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) { const ghostReadMarkers = this.state.ghostReadMarkers; ghostReadMarkers.push(prevProps.readMarkerEventId); this.setState({ ghostReadMarkers }); } } /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventNodes) { return undefined; } return this.eventNodes[eventId]; } /* return true if the content is fully scrolled down right now; else false. */ isAtBottom() { return this._scrollPanel.current && this._scrollPanel.current.isAtBottom(); } /* get the current scroll state. See ScrollPanel.getScrollState for * details. * * returns null if we are not mounted. */ getScrollState() { return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null; } // returns one of: // // null: there is no read marker // -1: read marker is above the window // 0: read marker is within the window // +1: read marker is below the window getReadMarkerPosition() { const readMarker = this._readMarkerNode.current; const messageWrapper = this._scrollPanel.current; if (!readMarker || !messageWrapper) { return null; } const wrapperRect = _reactDom.default.findDOMNode(messageWrapper).getBoundingClientRect(); const readMarkerRect = readMarker.getBoundingClientRect(); // the read-marker pretends to have zero height when it is actually // two pixels high; +2 here to account for that. if (readMarkerRect.bottom + 2 < wrapperRect.top) { return -1; } else if (readMarkerRect.top < wrapperRect.bottom) { return 0; } else { return 1; } } /* jump to the top of the content. */ scrollToTop() { if (this._scrollPanel.current) { this._scrollPanel.current.scrollToTop(); } } /* jump to the bottom of the content. */ scrollToBottom() { if (this._scrollPanel.current) { this._scrollPanel.current.scrollToBottom(); } } /** * Page up/down. * * @param {number} mult: -1 to page up, +1 to page down */ scrollRelative(mult) { if (this._scrollPanel.current) { this._scrollPanel.current.scrollRelative(mult); } } /** * Scroll up/down in response to a scroll key * * @param {KeyboardEvent} ev: the keyboard event to handle */ handleScrollKey(ev) { if (this._scrollPanel.current) { this._scrollPanel.current.handleScrollKey(ev); } } /* jump to the given event id. * * offsetBase gives the reference point for the pixelOffset. 0 means the * top of the container, 1 means the bottom, and fractional values mean * somewhere in the middle. If omitted, it defaults to 0. * * pixelOffset gives the number of pixels *above* the offsetBase that the * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ scrollToEvent(eventId, pixelOffset, offsetBase) { if (this._scrollPanel.current) { this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); } } scrollToEventIfNeeded(eventId) { const node = this.eventNodes[eventId]; if (node) { node.scrollIntoView({ block: "nearest", behavior: "instant" }); } } /* check the scroll state and send out pagination requests if necessary. */ checkFillState() { if (this._scrollPanel.current) { this._scrollPanel.current.checkFillState(); } } // TODO: Implement granular (per-room) hide options _shouldShowEvent(mxEv) { if (mxEv.sender && _MatrixClientPeg.MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) { return false; // ignored = no show (only happens if the ignore happens after an event was received) } if (this._showHiddenEventsInTimeline) { return true; } if (!(0, _EventTile.haveTileForEvent)(mxEv)) { return false; // no tile = no show } // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; return !(0, _shouldHideEvent.default)(mxEv); } _readMarkerForEvent(eventId, isLastEvent) { const visible = !isLastEvent && this.props.readMarkerVisible; if (this.props.readMarkerEventId === eventId) { let hr; // if the read marker comes at the end of the timeline (except // for local echoes, which are excluded from RMs, because they // don't have useful event ids), we don't want to show it, but // we still want to create the <li/> for it so that the // algorithms which depend on its position on the screen aren't // confused. if (visible) { hr = /*#__PURE__*/_react.default.createElement("hr", { className: "mx_RoomView_myReadMarker", style: { opacity: 1, width: '99%' } }); } return /*#__PURE__*/_react.default.createElement("li", { key: "readMarker_" + eventId, ref: this._readMarkerNode, className: "mx_RoomView_myReadMarker_container", "data-scroll-tokens": eventId }, hr); } else if (this.state.ghostReadMarkers.includes(eventId)) { // We render 'ghost' read markers in the DOM while they // transition away. This allows the actual read marker // to be in the right place straight away without having // to wait for the transition to finish. // There are probably much simpler ways to do this transition, // possibly using react-transition-group which handles keeping // elements in the DOM whilst they transition out, although our // case is a little more complex because only some of the items // transition (ie. the read markers do but the event tiles do not) // and TransitionGroup requires that all its children are Transitions. const hr = /*#__PURE__*/_react.default.createElement("hr", { className: "mx_RoomView_myReadMarker", ref: this._collectGhostReadMarker, onTransitionEnd: this._onGhostTransitionEnd, "data-eventid": eventId }); // give it a key which depends on the event id. That will ensure that // we get a new DOM node (restarting the animation) when the ghost // moves to a different event. return /*#__PURE__*/_react.default.createElement("li", { key: "_readuptoghost_" + eventId, className: "mx_RoomView_myReadMarker_container" }, hr); } return null; } _getNextEventInfo(arr, i) { const nextEvent = i < arr.length - 1 ? arr[i + 1] : null; // The next event with tile is used to to determine the 'last successful' flag // when rendering the tile. The shouldShowEvent function is pretty quick at what // it does, so this should have no significant cost even when a room is used for // not-chat purposes. const nextTile = arr.slice(i + 1).find(e => this._shouldShowEvent(e)); return { nextEvent, nextTile }; } get _roomHasPendingEdit() { return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`); } _getEventTiles() { this.eventNodes = {}; let i; // first figure out which is the last event in the list which we're // actually going to show; this allows us to behave slightly // differently for the last event in the list. (eg show timestamp) // // we also need to figure out which is the last event we show which isn't // a local echo, to manage the read-marker. let lastShownEvent; let lastShownNonLocalEchoIndex = -1; for (i = this.props.events.length - 1; i >= 0; i--) { const mxEv = this.props.events[i]; if (!this._shouldShowEvent(mxEv)) { continue; } if (lastShownEvent === undefined) { lastShownEvent = mxEv; } if (mxEv.status) { // this is a local echo continue; } lastShownNonLocalEchoIndex = i; break; } const ret = []; let prevEvent = null; // the last event we showed // Note: the EventTile might still render a "sent/sending receipt" independent of // this information. When not providing read receipt information, the tile is likely // to assume that sent receipts are to be shown more often. this._readReceiptsByEvent = {}; if (this.props.showReadReceipts) { this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); } let grouper = null; for (i = 0; i < this.props.events.length; i++) { const mxEv = this.props.events[i]; const eventId = mxEv.getId(); const last = mxEv === lastShownEvent; const { nextEvent, nextTile } = this._getNextEventInfo(this.props.events, i); if (grouper) { if (grouper.shouldGroup(mxEv)) { grouper.add(mxEv); continue; } else { // not part of group, so get the group tiles, close the // group, and continue like a normal event ret.push(...grouper.getTiles()); prevEvent = grouper.getNewPrevEvent(); grouper = null; } } for (const Grouper of groupers) { if (Grouper.canStartGroup(this, mxEv)) { grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); } } 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, isGrouped, nextEvent, nextTile)); prevEvent = mxEv; } const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); if (readMarker) ret.push(readMarker); } } if (!this.props.editState && this._roomHasPendingEdit) { _dispatcher.default.dispatch({ action: "edit_event", event: this.props.room.findEventById(this._roomHasPendingEdit) }); } if (grouper) { ret.push(...grouper.getTiles()); } return ret; } _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'); const ret = []; const isEditing = this.props.editState && this.props.editState.getEvent().getId() === mxEv.getId(); // local echoes have a fake date, which could even be yesterday. Treat them // as 'today' for the date separators. let ts1 = mxEv.getTs(); let eventDate = mxEv.getDate(); if (mxEv.status) { eventDate = new Date(); ts1 = eventDate.getTime(); } // do we need a date separator since the last event? const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate); if (wantsDateSeparator && !isGrouped) { const dateSeparator = /*#__PURE__*/_react.default.createElement("li", { key: ts1 }, /*#__PURE__*/_react.default.createElement(DateSeparator, { key: ts1, ts: ts1 })); ret.push(dateSeparator); } let willWantDateSeparator = false; if (nextEvent) { willWantDateSeparator = this._wantsDateSeparator(mxEv, nextEvent.getDate() || new Date()); } // is this a continuation of the previous message? const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv); const eventId = mxEv.getId(); const highlight = eventId === this.props.highlightedEventId; // we can't use local echoes as scroll tokens, because their event IDs change. // Local echos have a send "status". const scrollToken = mxEv.status ? undefined : eventId; const readReceipts = this._readReceiptsByEvent[eventId]; let isLastSuccessful = false; const isSentState = s => !s || s === 'sent'; const isSent = isSentState(mxEv.getAssociatedStatus()); const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent); if (!hasNextEvent && isSent) { isLastSuccessful = true; } else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) { isLastSuccessful = true; } // This is a bit nuanced, but if our next event is hidden but a future event is not // hidden then we're not the last successful. if (nextEventWithTile && nextEventWithTile !== nextEvent && isSentState(nextEventWithTile.getAssociatedStatus())) { isLastSuccessful = false; } // We only want to consider "last successful" if the event is sent by us, otherwise of course // it's successful: we received it. isLastSuccessful = isLastSuccessful && mxEv.getSender() === _MatrixClientPeg.MatrixClientPeg.get().getUserId(); // use txnId as key if available so that we don't remount during sending ret.push( /*#__PURE__*/_react.default.createElement("li", { key: mxEv.getTxnId() || eventId, ref: this._collectEventNode.bind(this, eventId), "data-scroll-tokens": scrollToken }, /*#__PURE__*/_react.default.createElement(TileErrorBoundary, { mxEvent: mxEv }, /*#__PURE__*/_react.default.createElement(EventTile, { mxEvent: mxEv, continuation: continuation, isRedacted: mxEv.isRedacted(), replacingEventId: mxEv.replacingEventId(), editState: isEditing && this.props.editState, onHeightChanged: this._onHeightChanged, readReceipts: readReceipts, readReceiptMap: this._readReceiptMap, showUrlPreview: this.props.showUrlPreview, checkUnmounting: this._isUnmounting, eventSendStatus: mxEv.getAssociatedStatus(), tileShape: this.props.tileShape, isTwelveHour: this.props.isTwelveHour, permalinkCreator: this.props.permalinkCreator, last: last, lastInSection: willWantDateSeparator, lastSuccessful: isLastSuccessful, isSelectedEvent: highlight, getRelationsForEvent: this.props.getRelationsForEvent, showReactions: this.props.showReactions, layout: this.props.layout, enableFlair: this.props.enableFlair, showReadReceipts: this.props.showReadReceipts })))); return ret; } _wantsDateSeparator(prevEvent, nextEventDate) { if (prevEvent == null) { // first event in the panel: depends if we could back-paginate from // here. return !this.props.suppressFirstDateSeparator; } return (0, _DateUtils.wantsDateSeparator)(prevEvent.getDate(), nextEventDate); } // Get a list of read receipts that should be shown next to this event // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. _getReadReceiptsForEvent(event) { const myUserId = _MatrixClientPeg.MatrixClientPeg.get().credentials.userId; // get list of read receipts, sorted most recent first const { room } = this.props; if (!room) { return null; } const receipts = []; room.getReceiptsForEvent(event).forEach(r => { if (!r.userId || r.type !== "m.read" || r.userId === myUserId) { return; // ignore non-read receipts and receipts from self. } if (_MatrixClientPeg.MatrixClientPeg.get().isUserIgnored(r.userId)) { return; // ignore ignored users } const member = room.getMember(r.userId); receipts.push({ userId: r.userId, roomMember: member, ts: r.data ? r.data.ts : 0 }); }); return receipts; } // Get an object that maps from event ID to a list of read receipts that // should be shown next to that event. If a hidden event has read receipts, // they are folded into the receipts of the last shown event. _getReadReceiptsByShownEvent() { const receiptsByEvent = {}; const receiptsByUserId = {}; let lastShownEventId; for (const event of this.props.events) { if (this._shouldShowEvent(event)) { lastShownEventId = event.getId(); } if (!lastShownEventId) { continue; } const existingReceipts = receiptsByEvent[lastShownEventId] || []; const newReceipts = this._getReadReceiptsForEvent(event); receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts); // Record these receipts along with their last shown event ID for // each associated user ID. for (const receipt of newReceipts) { receiptsByUserId[receipt.userId] = { lastShownEventId, receipt }; } } // It's possible in some cases (for example, when a read receipt // advances before we have paginated in the new event that it's marking // received) that we can temporarily not have a matching event for // someone which had one in the last. By looking through our previous // mapping of receipts by user ID, we can cover recover any receipts // that would have been lost by using the same event ID from last time. for (const userId in this._readReceiptsByUserId) { if (receiptsByUserId[userId]) { continue; } const { lastShownEventId, receipt } = this._readReceiptsByUserId[userId]; const existingReceipts = receiptsByEvent[lastShownEventId] || []; receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt); receiptsByUserId[userId] = { lastShownEventId, receipt }; } this._readReceiptsByUserId = receiptsByUserId; // After grouping receipts by shown events, do another pass to sort each // receipt list. for (const eventId in receiptsByEvent) { receiptsByEvent[eventId].sort((r1, r2) => { return r2.ts - r1.ts; }); } return receiptsByEvent; } updateTimelineMinHeight() { const scrollPanel = this._scrollPanel.current; if (scrollPanel) { const isAtBottom = scrollPanel.isAtBottom(); const whoIsTyping = this._whoIsTyping.current; const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); // when messages get added to the timeline, // but somebody else is still typing, // update the min-height, so once the last // person stops typing, no jumping occurs if (isAtBottom && isTypingVisible) { scrollPanel.preventShrinking(); } } } onTimelineReset() { const scrollPanel = this._scrollPanel.current; if (scrollPanel) { scrollPanel.clearPreventShrinking(); } } render() { const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary'); const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile"); const Spinner = sdk.getComponent("elements.Spinner"); let topSpinner; let bottomSpinner; if (this.props.backPaginating) { topSpinner = /*#__PURE__*/_react.default.createElement("li", { key: "_topSpinner" }, /*#__PURE__*/_react.default.createElement(Spinner, null)); } if (this.props.forwardPaginating) { bottomSpinner = /*#__PURE__*/_react.default.createElement("li", { key: "_bottomSpinner" }, /*#__PURE__*/_react.default.createElement(Spinner, null)); } const style = this.props.hidden ? { display: 'none' } : {}; const className = (0, _classnames.default)(this.props.className, { "mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps }); let whoIsTyping; if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) { whoIsTyping = /*#__PURE__*/_react.default.createElement(WhoIsTypingTile, { room: this.props.room, onShown: this._onTypingShown, onHidden: this._onTypingHidden, ref: this._whoIsTyping }); } let ircResizer = null; if (this.props.layout == _Layout.Layout.IRC) { ircResizer = /*#__PURE__*/_react.default.createElement(_IRCTimelineProfileResizer.default, { minWidth: 20, maxWidth: 600, roomId: this.props.room ? this.props.room.roomId : null }); } return /*#__PURE__*/_react.default.createElement(ErrorBoundary, null, /*#__PURE__*/_react.default.createElement(ScrollPanel, { ref: this._scrollPanel, className: className, onScroll: this.props.onScroll, onResize: this.onResize, onFillRequest: this.props.onFillRequest, onUnfillRequest: this.props.onUnfillRequest, style: style, stickyBottom: this.props.stickyBottom, resizeNotifier: this.props.resizeNotifier, fixedChildren: ircResizer }, topSpinner, this._getEventTiles(), whoIsTyping, bottomSpinner)); } }, (0, _defineProperty2.default)(_class2, "propTypes", { // true to give the component a 'display: none' style. hidden: _propTypes.default.bool, // true to show a spinner at the top of the timeline to indicate // back-pagination in progress backPaginating: _propTypes.default.bool, // true to show a spinner at the end of the timeline to indicate // forward-pagination in progress forwardPaginating: _propTypes.default.bool, // the list of MatrixEvents to display events: _propTypes.default.array.isRequired, // ID of an event to highlight. If undefined, no event will be highlighted. highlightedEventId: _propTypes.default.string, // The room these events are all in together, if any. // (The notification panel won't have a room here, for example.) room: _propTypes.default.object, // Should we show URL Previews showUrlPreview: _propTypes.default.bool, // event after which we should show a read marker readMarkerEventId: _propTypes.default.string, // whether the read marker should be visible readMarkerVisible: _propTypes.default.bool, // the userid of our user. This is used to suppress the read marker // for pending messages. ourUserId: _propTypes.default.string, // true to suppress the date at the start of the timeline suppressFirstDateSeparator: _propTypes.default.bool, // whether to show read receipts showReadReceipts: _propTypes.default.bool, // true if updates to the event list should cause the scroll panel to // scroll down when we are at the bottom of the window. See ScrollPanel // for more details. stickyBottom: _propTypes.default.bool, // callback which is called when the panel is scrolled. onScroll: _propTypes.default.func, // callback which is called when more content is needed. onFillRequest: _propTypes.default.func, // className for the panel className: _propTypes.default.string.isRequired, // shape parameter to be passed to EventTiles tileShape: _propTypes.default.string, // show twelve hour timestamps isTwelveHour: _propTypes.default.bool, // show timestamps always alwaysShowTimestamps: _propTypes.default.bool, // helper function to access relations for an event getRelationsForEvent: _propTypes.default.func, // whether to show reactions for an event showReactions: _propTypes.default.bool, // which layout to use layout: _Layout.LayoutPropType, // whether or not to show flair at all enableFlair: _propTypes.default.bool }), _temp)) || _class); exports.default = MessagePanel; /* Grouper classes determine when events can be grouped together in a summary. * Groupers should have the following methods: * - canStartGroup (static): determines if a new group should be started with the * given event * - shouldGroup: determines if the given event should be added to an existing group * - add: adds an event to an existing group (should only be called if shouldGroup * return true) * - getTiles: returns the tiles that represent the group * - getNewPrevEvent: returns the event that should be used as the new prevEvent * when determining things such as whether a date separator is necessary */ // Wrap initial room creation events into an EventListSummary // Grouping only events sent by the same user that sent the `m.room.create` and only until // the first non-state event or membership event which is not regarding the sender of the `m.room.create` event class CreationGrouper { constructor(panel, createEvent, prevEvent, lastShownEvent) { this.panel = panel; this.createEvent = createEvent; this.prevEvent = prevEvent; this.lastShownEvent = lastShownEvent; this.events = []; // events that we include in the group but then eject out and place // above the group. this.ejectedEvents = []; this.readMarker = panel._readMarkerForEvent(createEvent.getId(), createEvent === lastShownEvent); } shouldGroup(ev) { const panel = this.panel; const createEvent = this.createEvent; if (!panel._shouldShowEvent(ev)) { return true; } if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) { return false; } if (ev.getType() === "m.room.member" && (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) { return false; } if (ev.isState() && ev.getSender() === createEvent.getSender()) { return true; } return false; } add(ev) { const panel = this.panel; this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId(), ev === this.lastShownEvent); if (!panel._shouldShowEvent(ev)) { return; } if (ev.getType() === "m.room.encryption") { this.ejectedEvents.push(ev); } else { this.events.push(ev); } } getTiles() { // If we don't have any events to group, don't even try to group them. The logic // below assumes that we have a group of events to deal with, but we might not if // the events we were supposed to group were redacted. if (!this.events || !this.events.length) return []; 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; if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) { const ts = createEvent.getTs(); ret.push( /*#__PURE__*/_react.default.createElement("li", { key: ts + '~' }, /*#__PURE__*/_react.default.createElement(DateSeparator, { key: ts + '~', ts: ts }))); } // 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)); } for (const ejected of this.ejectedEvents) { ret.push(...panel._getTilesForEvent(createEvent, ejected, createEvent === lastShownEvent, isGrouped)); } const eventTiles = this.events.map(e => { // In order to prevent DateSeparators from appearing in the expanded form // 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, 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]; let summaryText; const roomId = ev.getRoomId(); const creator = ev.sender ? ev.sender.name : ev.getSender(); if (_DMRoomMap.default.shared().getUserIdForRoomId(roomId)) { summaryText = (0, _languageHandler._t)("%(creator)s created this DM.", { creator }); } else { summaryText = (0, _languageHandler._t)("%(creator)s created and configured the room.", { creator }); } ret.push( /*#__PURE__*/_react.default.createElement(_NewRoomIntro.default, { key: "newroomintro" })); ret.push( /*#__PURE__*/_react.default.createElement(EventListSummary, { key: "roomcreationsummary", events: this.events, onToggle: panel._onHeightChanged // Update scroll state , summaryMembers: [ev.sender], summaryText: summaryText }, eventTiles)); if (this.readMarker) { ret.push(this.readMarker); } return ret; } getNewPrevEvent() { return this.createEvent; } } (0, _defineProperty2.default)(CreationGrouper, "canStartGroup", function (panel, ev) { return ev.getType() === "m.room.create"; }); class RedactionGrouper { constructor(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile) { this.panel = panel; this.readMarker = panel._readMarkerForEvent(ev.getId(), ev === lastShownEvent); this.events = [ev]; this.prevEvent = prevEvent; this.lastShownEvent = lastShownEvent; this.nextEvent = nextEvent; this.nextEventTile = nextEventTile; } shouldGroup(ev) { // absorb hidden events so that they do not break up streams of messages & redaction events being grouped if (!this.panel._shouldShowEvent(ev)) { return true; } if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { return false; } return ev.isRedacted(); } add(ev) { this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId(), ev === this.lastShownEvent); if (!this.panel._shouldShowEvent(ev)) { return; } this.events.push(ev); } getTiles() { if (!this.events || !this.events.length) return []; 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; if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { const ts = this.events[0].getTs(); ret.push( /*#__PURE__*/_react.default.createElement("li", { key: ts + '~' }, /*#__PURE__*/_react.default.createElement(DateSeparator, { key: ts + '~', ts: ts }))); } const key = "redactioneventlistsummary-" + (this.prevEvent ? this.events[0].getId() : "initial"); const senders = new Set(); 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, isGrouped, this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { eventTiles = null; } ret.push( /*#__PURE__*/_react.default.createElement(EventListSummary, { key: key, threshold: 2, events: this.events, onToggle: panel._onHeightChanged // Update scroll state , summaryMembers: Array.from(senders), summaryText: (0, _languageHandler._t)("%(count)s messages deleted.", { count: eventTiles.length }) }, eventTiles)); if (this.readMarker) { ret.push(this.readMarker); } return ret; } getNewPrevEvent() { return this.events[this.events.length - 1]; } } // Wrap consecutive member events in a ListSummary, ignore if redacted (0, _defineProperty2.default)(RedactionGrouper, "canStartGroup", function (panel, ev) { return panel._shouldShowEvent(ev) && ev.isRedacted(); }); class MemberGrouper { constructor(panel, ev, prevEvent, lastShownEvent) { this.panel = panel; this.readMarker = panel._readMarkerForEvent(ev.getId(), ev === lastShownEvent); this.events = [ev]; this.prevEvent = prevEvent; this.lastShownEvent = lastShownEvent; } shouldGroup(ev) { if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { return false; } return isMembershipChange(ev); } add(ev) { if (ev.getType() === 'm.room.member') { // We'll just double check that it's worth our time to do so, through an // ugly hack. If textForEvent returns something, we should group it for // rendering but if it doesn't then we'll exclude it. const renderText = (0, _TextForEvent.textForEvent)(ev); if (!renderText || renderText.trim().length === 0) return; // quietly ignore } this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId(), ev === this.lastShownEvent); this.events.push(ev); } getTiles() { // If we don't have any events to group, don't even try to group them. The logic // below assumes that we have a group of events to deal with, but we might not if // the events we were supposed to group were redacted. if (!this.events || !this.events.length) return []; 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 = []; if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { const ts = this.events[0].getTs(); ret.push( /*#__PURE__*/_react.default.createElement("li", { key: ts + '~' }, /*#__PURE__*/_react.default.createElement(DateSeparator, { key: ts + '~', ts: ts }))); } // Ensure that the key of the MemberEventListSummary does not change with new // member events. This will prevent it from being re-created unnecessarily, and // instead will allow new props to be provided. In turn, the shouldComponentUpdate // method on MELS can be used to prevent unnecessary renderings. // // Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null, // so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first // membership event, which will not change during forward pagination. const key = "membereventlistsummary-" + (this.prevEvent ? this.events[0].getId() : "initial"); let highlightInMels; let eventTiles = this.events.map(e => { if (e.getId() === panel.props.highlightedEventId) { highlightInMels = true; } // In order to prevent DateSeparators from appearing in the expanded form // 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, isGrouped); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { eventTiles = null; } ret.push( /*#__PURE__*/_react.default.createElement(MemberEventListSummary, { key: key, events: this.events, onToggle: panel._onHeightChanged // Update scroll state , startExpanded: highlightInMels }, eventTiles)); if (this.readMarker) { ret.push(this.readMarker); } return ret; } getNewPrevEvent() { return this.events[0]; } } // all the grouper classes that we use (0, _defineProperty2.default)(MemberGrouper, "canStartGroup", function (panel, ev) { return panel._shouldShowEvent(ev) && isMembershipChange(ev); }); const groupers = [CreationGrouper, MemberGrouper, RedactionGrouper]; //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL3N0cnVjdHVyZXMvTWVzc2FnZVBhbmVsLmpzIl0sIm5hbWVzIjpbIkNPTlRJTlVBVElPTl9NQVhfSU5URVJWQUwiLCJjb250aW51ZWRUeXBlcyIsInNob3VsZEZvcm1Db250aW51YXRpb24iLCJwcmV2RXZlbnQiLCJteEV2ZW50Iiwic2VuZGVyIiwiZ2V0VHMiLCJpc1JlZGFjdGVkIiwiZ2V0VHlwZSIsImluY2x1ZGVzIiwidXNlcklkIiwibmFtZSIsImdldE14Y0F2YXRhclVybCIsImlzTWVtYmVyc2hpcENoYW5nZSIsImUiLCJNZXNzYWdlUGFuZWwiLCJSZWFjdCIsIkNvbXBvbmVudCIsImNvbnN0cnVjdG9yIiwicHJvcHMiLCJzZXRTdGF0ZSIsInNob3dUeXBpbmdOb3RpZmljYXRpb25zIiwiU2V0dGluZ3NTdG9yZSIsImdldFZhbHVlIiwiX2lzTW91bnRlZCIsIm5vZGUiLCJyZXF1ZXN0QW5pbWF0aW9uRnJhbWUiLCJzdHlsZSIsIndpZHRoIiwib3BhY2l0eSIsImV2IiwiZmluaXNoZWRFdmVudElkIiwidGFyZ2V0IiwiZGF0YXNldCIsImV2ZW50aWQiLCJnaG9zdFJlYWRNYXJrZXJzIiwic3RhdGUiLCJmaWx0ZXIiLCJlaWQiLCJldmVudElkIiwiZXZlbnROb2RlcyIsInNjcm9sbFBhbmVsIiwiX3Njcm9sbFBhbmVsIiwiY3VycmVudCIsImNoZWNrU2Nyb2xsIiwiZ2V0U2Nyb2xsU3RhdGUiLCJzdHVja0F0Qm90dG9tIiwicHJldmVudFNocmlua2luZyIsInVwZGF0ZVByZXZlbnRTaHJpbmtpbmciLCJfcmVhZFJlY2VpcHRNYXAiLCJfcmVhZFJlY2VpcHRzQnlFdmVudCIsIl9yZWFkUmVjZWlwdHNCeVVzZXJJZCIsIl9zaG93SGlkZGVuRXZlbnRzSW5UaW1lbGluZSIsIl9yZWFkTWFya2VyTm9kZSIsIl93aG9Jc1R5cGluZyIsIl9zaG93VHlwaW5nTm90aWZpY2F0aW9uc1dhdGNoZXJSZWYiLCJ3YXRjaFNldHRpbmciLCJvblNob3dUeXBpbmdOb3RpZmljYXRpb25zQ2hhbmdlIiwiY29tcG9uZW50RGlkTW91bnQiLCJjb21wb25lbnRXaWxsVW5tb3VudCIsInVud2F0Y2hTZXR0aW5nIiwiY29tcG9uZW50RGlkVXBkYXRlIiwicHJldlByb3BzIiwicHJldlN0YXRlIiwicmVhZE1hcmtlclZpc2libGUiLCJyZWFkTWFya2VyRXZlbnRJZCIsInB1c2giLCJnZXROb2RlRm9yRXZlbnRJZCIsInVuZGVmaW5lZCIsImlzQXRCb3R0b20iLCJnZXRSZWFkTWFya2VyUG9zaXRpb24iLCJyZWFkTWFya2VyIiwibWVzc2FnZVdyYXBwZXIiLCJ3cmFwcGVyUmVjdCIsIlJlYWN0RE9NIiwiZmluZERPTU5vZGUiLCJnZXRCb3VuZGluZ0NsaWVudFJlY3QiLCJyZWFkTWFya2VyUmVjdCIsImJvdHRvbSIsInRvcCIsInNjcm9sbFRvVG9wIiwic2Nyb2xsVG9Cb3R0b20iLCJzY3JvbGxSZWxhdGl2ZSIsIm11bHQiLCJoYW5kbGVTY3JvbGxLZXkiLCJzY3JvbGxUb0V2ZW50IiwicGl4ZWxPZmZzZXQiLCJvZmZzZXRCYXNlIiwic2Nyb2xsVG9Ub2tlbiIsInNjcm9sbFRvRXZlbnRJZk5lZWRlZCIsInNjcm9sbEludG9WaWV3IiwiYmxvY2siLCJiZWhhdmlvciIsImNoZWNrRmlsbFN0YXRlIiwiX3Nob3VsZFNob3dFdmVudCIsIm14RXYiLCJNYXRyaXhDbGllbnRQZWciLCJnZXQiLCJpc1VzZXJJZ25vcmVkIiwiaGlnaGxpZ2h0ZWRFdmVudElkIiwiZ2V0SWQiLCJfcmVhZE1hcmtlckZvckV2ZW50IiwiaXNMYXN0RXZlbnQiLCJ2aXNpYmxlIiwiaHIiLCJfY29sbGVjdEdob3N0UmVhZE1hcmtlciIsIl9vbkdob3N0VHJhbnNpdGlvbkVuZCIsIl9nZXROZXh0RXZlbnRJbmZvIiwiYXJyIiwiaSIsIm5leHRFdmVudCIsImxlbmd0aCIsIm5leHRUaWxlIiwic2xpY2UiLCJmaW5kIiwiX3Jvb21IYXNQZW5kaW5nRWRpdCIsInJvb20iLCJsb2NhbFN0b3JhZ2UiLCJnZXRJdGVtIiwicm9vbUlkIiwiX2dldEV2ZW50VGlsZXMiLCJsYXN0U2hvd25FdmVudCIsImxhc3RTaG93bk5vbkxvY2FsRWNob0luZGV4IiwiZXZlbnRzIiwic3RhdHVzIiwicmV0Iiwic2hvd1JlYWRSZWNlaXB0cyIsIl9nZXRSZWFkUmVjZWlwdHNCeVNob3duRXZlbnQiLCJncm91cGVyIiwibGFzdCIsInNob3VsZEdyb3VwIiwiYWRkIiwiZ2V0VGlsZXMiLCJnZXROZXdQcmV2RXZlbnQiLCJHcm91cGVyIiwiZ3JvdXBlcnMiLCJjYW5TdGFydEdyb3VwIiwid2FudFRpbGUiLCJpc0dyb3VwZWQiLCJfZ2V0VGlsZXNGb3JFdmVudCIsImVkaXRTdGF0ZSIsImRlZmF1bHREaXNwYXRjaGVyIiwiZGlzcGF0Y2giLCJhY3Rpb24iLCJldmVudCIsImZpbmRFdmVudEJ5SWQiLCJuZXh0RXZlbnRXaXRoVGlsZSIsIlRpbGVFcnJvckJvdW5kYXJ5Iiwic2RrIiwiZ2V0Q29tcG9uZW50IiwiRXZlbnRUaWxlIiwiRGF0ZVNlcGFyYXRvciIsImlzRWRpdGluZyIsImdldEV2ZW50IiwidHMxIiwiZXZlbnREYXRlIiwiZ2V0RGF0ZSIsIkRhdGUiLCJnZXRUaW1lIiwid2FudHNEYXRlU2VwYXJhdG9yIiwiX3dhbnRzRGF0ZVNlcGFyYXRvciIsImRhdGVTZXBhcmF0b3IiLCJ3aWxsV2FudERhdGVTZXBhcmF0b3IiLCJjb250aW51YXRpb24iLCJoaWdobGlnaHQiLCJzY3JvbGxUb2tlbiIsInJlYWRSZWNlaXB0cyIsImlzTGFzdFN1Y2Nlc3NmdWwiLCJpc1NlbnRTdGF0ZSIsInMiLCJpc1NlbnQiLCJnZXRBc3NvY2lhdGVkU3RhdHVzIiwiaGFzTmV4dEV2ZW50IiwiZ2V0U2VuZGVyIiwiZ2V0VXNlcklkIiwiZ2V0VHhuSWQiLCJfY29sbGVjdEV2ZW50Tm9kZSIsImJpbmQiLCJyZXBsYWNpbmdFdmVudElkIiwiX29uSGVpZ2h0Q2hhbmdlZCIsInNob3dVcmxQcmV2aWV3IiwiX2lzVW5tb3VudGluZyIsInRpbGVTaGFwZSIsImlzVHdlbHZlSG91ciIsInBlcm1hbGlua0NyZWF0b3IiLCJnZXRSZWxhdGlvbnNGb3JFdmVudCIsInNob3dSZWFjdGlvbnMiLCJsYXlvdXQiLCJlbmFibGVGbGFpciIsIm5leHRFdmVudERhdGUiLCJzdXBwcmVzc0ZpcnN0RGF0ZVNlcGFyYXRvciIsIl9nZXRSZWFkUmVjZWlwdHNGb3JFdmVudCIsIm15VXNlcklkIiwiY3JlZGVudGlhbHMiLCJyZWNlaXB0cyIsImdldFJlY2VpcHRzRm9yRXZlbnQiLCJmb3JFYWNoIiwiciIsInR5cGUiLCJtZW1iZXIiLCJnZXRNZW1iZXIiLCJyb29tTWVtYmVyIiwidHMiLCJkYXRhIiwicmVjZWlwdHNCeUV2ZW50IiwicmVjZWlwdHNCeVVzZXJJZCIsImxhc3RTaG93bkV2ZW50SWQiLCJleGlzdGluZ1JlY2VpcHRzIiwibmV3UmVjZWlwdHMiLCJjb25jYXQiLCJyZWNlaXB0Iiwic29ydCIsInIxIiwicjIiLCJ1cGRhdGVUaW1lbGluZU1pbkhlaWdodCIsIndob0lzVHlwaW5nIiwiaXNUeXBpbmdWaXNpYmxlIiwiaXNWaXNpYmxlIiwib25UaW1lbGluZVJlc2V0IiwiY2xlYXJQcmV2ZW50U2hyaW5raW5nIiwicmVuZGVyIiwiRXJyb3JCb3VuZGFyeSIsIlNjcm9sbFBhbmVsIiwiV2hvSXNUeXBpbmdUaWxlIiwiU3Bpbm5lciIsInRvcFNwaW5uZXIiLCJib3R0b21TcGlubmVyIiwiYmFja1BhZ2luYXRpbmciLCJmb3J3YXJkUGFnaW5hdGluZyIsImhpZGRlbiIsImRpc3BsYXkiLCJjbGFzc05hbWUiLCJhbHdheXNTaG93VGltZXN0YW1wcyIsIl9vblR5cGluZ1Nob3duIiwiX29uVHlwaW5nSGlkZGVuIiwiaXJjUmVzaXplciIsIkxheW91dCIsIklSQyIsIm9uU2Nyb2xsIiwib25SZXNpemUiLCJvbkZpbGxSZXF1ZXN0Iiwib25VbmZpbGxSZXF1ZXN0Iiwic3RpY2t5Qm90dG9tIiwicmVzaXplTm90aWZpZXIiLCJQcm9wVHlwZXMiLCJib29sIiwiYXJyYXkiLCJpc1JlcXVpcmVkIiwic3RyaW5nIiwib2JqZWN0Iiwib3VyVXNlcklkIiwiZnVuYyIsIkxheW91dFByb3BUeXBlIiwiQ3JlYXRpb25Hcm91cGVyIiwicGFuZWwiLCJjcmVhdGVFdmVudCIsImVqZWN0ZWRFdmVudHMiLCJnZXRTdGF0ZUtleSIsImdldENvbnRlbnQiLCJpc1N0YXRlIiwiRXZlbnRMaXN0U3VtbWFyeSIsImVqZWN0ZWQiLCJldmVudFRpbGVzIiwibWFwIiwicmVkdWNlIiwiYSIsImIiLCJzdW1tYXJ5VGV4dCIsImdldFJvb21JZCIsImNyZWF0b3IiLCJETVJvb21NYXAiLCJzaGFyZWQiLCJnZXRVc2VySWRGb3JSb29tSWQiLCJSZWRhY3Rpb25Hcm91cGVyIiwibmV4dEV2ZW50VGlsZSIsImtleSIsInNlbmRlcnMiLCJTZXQiLCJBcnJheSIsImZyb20iLCJjb3VudCIsIk1lbWJlckdyb3VwZXIiLCJyZW5kZXJUZXh0IiwidHJpbSIsIk1lbWJlckV2ZW50TGlzdFN1bW1hcnkiLCJoaWdobGlnaHRJbk1lbHMiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7QUFrQkE7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBRUE7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7Ozs7QUFFQSxNQUFNQSx5QkFBeUIsR0FBRyxJQUFJLEVBQUosR0FBUyxJQUEzQyxDLENBQWlEOztBQUNqRCxNQUFNQyxjQUFjLEdBQUcsQ0FBQyxXQUFELEVBQWMsZ0JBQWQsQ0FBdkIsQyxDQUVBO0FBQ0E7O0FBQ0EsU0FBU0Msc0JBQVQsQ0FBZ0N