UNPKG

matrix-react-sdk

Version:
859 lines (824 loc) 136 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; exports.shouldFormContinuation = shouldFormContinuation; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _reactDom = _interopRequireDefault(require("react-dom")); var _classnames = _interopRequireDefault(require("classnames")); var _matrix = require("matrix-js-sdk/src/matrix"); var _logger = require("matrix-js-sdk/src/logger"); var _utils = require("matrix-js-sdk/src/utils"); var _shouldHideEvent = _interopRequireDefault(require("../../shouldHideEvent")); var _DateUtils = require("../../DateUtils"); var _MatrixClientPeg = require("../../MatrixClientPeg"); var _SettingsStore = _interopRequireDefault(require("../../settings/SettingsStore")); var _RoomContext = _interopRequireWildcard(require("../../contexts/RoomContext")); var _Layout = require("../../settings/enums/Layout"); var _EventTile = _interopRequireWildcard(require("../views/rooms/EventTile")); var _IRCTimelineProfileResizer = _interopRequireDefault(require("../views/elements/IRCTimelineProfileResizer")); var _dispatcher = _interopRequireDefault(require("../../dispatcher/dispatcher")); var _WhoIsTypingTile = _interopRequireDefault(require("../views/rooms/WhoIsTypingTile")); var _ScrollPanel = _interopRequireDefault(require("./ScrollPanel")); var _DateSeparator = _interopRequireDefault(require("../views/messages/DateSeparator")); var _TimelineSeparator = _interopRequireWildcard(require("../views/messages/TimelineSeparator")); var _ErrorBoundary = _interopRequireDefault(require("../views/elements/ErrorBoundary")); var _Spinner = _interopRequireDefault(require("../views/elements/Spinner")); var _actions = require("../../dispatcher/actions"); var _EventRenderingUtils = require("../../utils/EventRenderingUtils"); var _EventTileFactory = require("../../events/EventTileFactory"); var _Editing = require("../../Editing"); var _EventUtils = require("../../utils/EventUtils"); var _MainGrouper = require("./grouper/MainGrouper"); var _CreationGrouper = require("./grouper/CreationGrouper"); var _languageHandler = require("../../languageHandler"); var _LateEventGrouper = require("./grouper/LateEventGrouper"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /* Copyright 2024 New Vector Ltd. Copyright 2016-2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [_matrix.EventType.Sticker, _matrix.EventType.RoomMessage]; // 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, matrixClient, showHiddenEvents, timelineRenderingType) { if (timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList) return false; // sanity check inputs if (!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; // Thread summaries in the main timeline should break up a continuation on both sides if (((0, _EventUtils.hasThreadSummary)(mxEvent) || (0, _EventUtils.hasThreadSummary)(prevEvent)) && timelineRenderingType !== _RoomContext.TimelineRenderingType.Thread) { return false; } // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile if (!(0, _EventTileFactory.haveRendererForEvent)(prevEvent, matrixClient, showHiddenEvents)) return false; return true; } /* (almost) stateless UI component which builds the event tiles in the room timeline. */ class MessagePanel extends _react.default.Component { constructor(props, context) { super(props, context); // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations (0, _defineProperty2.default)(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. (0, _defineProperty2.default)(this, "readReceiptsByEvent", new Map()); // 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. (0, _defineProperty2.default)(this, "readReceiptsByUserId", new Map()); (0, _defineProperty2.default)(this, "_showHiddenEvents", void 0); (0, _defineProperty2.default)(this, "isMounted", false); (0, _defineProperty2.default)(this, "readMarkerNode", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "whoIsTyping", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "scrollPanel", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "showTypingNotificationsWatcherRef", void 0); (0, _defineProperty2.default)(this, "eventTiles", {}); // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. (0, _defineProperty2.default)(this, "grouperKeyMap", new WeakMap()); (0, _defineProperty2.default)(this, "calculateRoomMembersCount", () => { this.setState({ hideSender: this.shouldHideSender() }); }); (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, "collectEventTile", (eventId, node) => { this.eventTiles[eventId] = node; }); // once dynamic content in the events load, make the scrollPanel check the // scroll offsets. (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"), hideSender: this.shouldHideSender() }; // Cache these settings on mount since Settings is expensive to query, // and we check this in a hot code path. This is also cached in our // RoomContext, however we still need a fallback for roomless MessagePanels. this._showHiddenEvents = _SettingsStore.default.getValue("showHiddenEventsInTimeline"); this.showTypingNotificationsWatcherRef = _SettingsStore.default.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); } componentDidMount() { this.calculateRoomMembersCount(); this.props.room?.currentState.on(_matrix.RoomStateEvent.Update, this.calculateRoomMembersCount); this.isMounted = true; } componentWillUnmount() { this.isMounted = false; this.props.room?.currentState.off(_matrix.RoomStateEvent.Update, this.calculateRoomMembersCount); _SettingsStore.default.unwatchSetting(this.showTypingNotificationsWatcherRef); this.readReceiptMap = {}; } componentDidUpdate(prevProps, prevState) { if (prevProps.layout !== this.props.layout) { this.calculateRoomMembersCount(); } if (prevProps.readMarkerVisible && prevProps.readMarkerEventId && this.props.readMarkerEventId !== prevProps.readMarkerEventId) { const ghostReadMarkers = this.state.ghostReadMarkers; ghostReadMarkers.push(prevProps.readMarkerEventId); this.setState({ ghostReadMarkers }); } const pendingEditItem = this.pendingEditItem; if (!this.props.editState && this.props.room && pendingEditItem) { const event = this.props.room.findEventById(pendingEditItem); _dispatcher.default.dispatch({ action: _actions.Action.EditEvent, event: !event?.isRedacted() ? event : null, timelineRenderingType: this.context.timelineRenderingType }); } } shouldHideSender() { return !!this.props.room && this.props.room.getInvitedAndJoinedMemberCount() <= 2 && this.props.layout === _Layout.Layout.Bubble; } /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventTiles) { return undefined; } return this.eventTiles[eventId]?.ref?.current ?? undefined; } getTileForEventId(eventId) { if (!this.eventTiles || !eventId) { return undefined; } return this.eventTiles[eventId]; } /* return true if the content is fully scrolled down right now; else false. */ isAtBottom() { return 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?.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() { this.scrollPanel.current?.scrollToTop(); } /* jump to the bottom of the content. */ scrollToBottom() { this.scrollPanel.current?.scrollToBottom(); } /** * Scroll up/down in response to a scroll key * * @param {KeyboardEvent} ev: the keyboard event to handle */ handleScrollKey(ev) { 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) { this.scrollPanel.current?.scrollToToken(eventId, pixelOffset, offsetBase); } scrollToEventIfNeeded(eventId) { const node = this.getNodeForEventId(eventId); if (node) { node.scrollIntoView({ block: "nearest", behavior: "instant" }); } } get showHiddenEvents() { return this.context?.showHiddenEvents ?? this._showHiddenEvents; } // TODO: Implement granular (per-room) hide options shouldShowEvent(mxEv, forceHideEvents = false) { if (this.props.hideThreadedMessages && this.props.room) { const { shouldLiveInRoom } = this.props.room.eventShouldLiveIn(mxEv, this.props.events); if (!shouldLiveInRoom) { return false; } } if (_MatrixClientPeg.MatrixClientPeg.safeGet().isUserIgnored(mxEv.getSender())) { return false; // ignored = no show (only happens if the ignore happens after an event was received) } if (this.showHiddenEvents && !forceHideEvents) { return true; } if (!(0, _EventTileFactory.haveRendererForEvent)(mxEv, _MatrixClientPeg.MatrixClientPeg.safeGet(), this.showHiddenEvents)) { return false; // no tile = no show } // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; return !(0, _shouldHideEvent.default)(mxEv, this.context); } readMarkerForEvent(eventId, isLastEvent) { if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.File) return null; 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", { style: { opacity: 1, width: "99%" } }); } return /*#__PURE__*/_react.default.createElement("li", { key: "readMarker_" + eventId, ref: this.readMarkerNode, className: "mx_MessagePanel_myReadMarker", "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", { 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_MessagePanel_myReadMarker" }, hr); } return null; } /** * Find the next event in the list, and the next visible event in the list. * * @param events - the list of events to look in and whether they are shown * @param i - where in the list we are now * * @returns { nextEvent, nextTile } * * nextEvent is the event after i in the supplied array. * * nextTile is the first event in the array after i that we will show a tile * for. It is used to to determine the 'last successful' flag when rendering * the tile. */ getNextEventInfo(events, i) { // WARNING: this method is on a hot path. const nextEventAndShouldShow = i < events.length - 1 ? events[i + 1] : null; const nextTile = findFirstShownAfter(i, events); return { nextEventAndShouldShow, nextTile }; } get pendingEditItem() { if (!this.props.room) { return null; } try { return localStorage.getItem((0, _Editing.editorRoomKey)(this.props.room.roomId, this.context.timelineRenderingType)); } catch (err) { _logger.logger.error(err); return null; } } isSentState(ev) { const status = ev.getAssociatedStatus(); // A falsey state applies to events which have come down sync, including remote echoes return !status || status === _matrix.EventStatus.SENT; } getEventTiles() { // 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; const events = this.props.events.map(event => { return { event, shouldShow: this.shouldShowEvent(event) }; }); const userId = _MatrixClientPeg.MatrixClientPeg.safeGet().getSafeUserId(); let foundLastSuccessfulEvent = false; let lastShownNonLocalEchoIndex = -1; // Find the indices of the last successful event we sent and the last non-local-echo events shown for (let i = events.length - 1; i >= 0; i--) { const { event, shouldShow } = events[i]; if (!shouldShow) { continue; } if (lastShownEvent === undefined) { lastShownEvent = event; } if (!foundLastSuccessfulEvent && this.isSentState(event) && (0, _EventTile.isEligibleForSpecialReceipt)(event)) { foundLastSuccessfulEvent = true; // If we are not sender of this last successful event eligible for special receipt then we stop here // As we do not want to render our sent receipt if there are more receipts below it and events sent // by other users get a synthetic read receipt for their sent events. if (event.getSender() === userId) { events[i].lastSuccessfulWeSent = true; } } if (lastShownNonLocalEchoIndex < 0 && !event.status) { lastShownNonLocalEchoIndex = i; } if (lastShownNonLocalEchoIndex >= 0 && foundLastSuccessfulEvent) { 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 = new Map(); if (this.props.showReadReceipts) { this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(events); } let grouper = null; for (let i = 0; i < events.length; i++) { const wrappedEvent = events[i]; const { event, shouldShow } = wrappedEvent; const eventId = event.getId(); const last = event === lastShownEvent; const { nextEventAndShouldShow, nextTile } = this.getNextEventInfo(events, i); if (grouper) { if (grouper.shouldGroup(wrappedEvent)) { grouper.add(wrappedEvent); 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, wrappedEvent) && !this.props.disableGrouping) { grouper = new Grouper(this, wrappedEvent, prevEvent, lastShownEvent, nextEventAndShouldShow, nextTile); break; // break on first grouper } } if (!grouper) { if (shouldShow) { // make sure we unpack the array returned by getTilesForEvent, // otherwise React will auto-generate keys, and we will end up // replacing all the DOM elements every time we paginate. ret.push(...this.getTilesForEvent(prevEvent, wrappedEvent, last, false, nextEventAndShouldShow, nextTile)); prevEvent = event; } const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); if (readMarker) ret.push(readMarker); } } if (grouper) { ret.push(...grouper.getTiles()); } return ret; } getTilesForEvent(prevEvent, wrappedEvent, last = false, isGrouped = false, nextEvent = null, nextEventWithTile = null) { const mxEv = wrappedEvent.event; const ret = []; const isEditing = 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. const ts1 = mxEv.getTs() ?? Date.now(); // do we need a separator since the last event? const wantsSeparator = this.wantsSeparator(prevEvent, mxEv); if (!isGrouped && this.props.room) { if (wantsSeparator === _TimelineSeparator.SeparatorKind.Date) { ret.push( /*#__PURE__*/_react.default.createElement("li", { key: ts1 }, /*#__PURE__*/_react.default.createElement(_DateSeparator.default, { key: ts1, roomId: this.props.room.roomId, ts: ts1 }))); } else if (wantsSeparator === _TimelineSeparator.SeparatorKind.LateEvent) { const text = (0, _languageHandler._t)("timeline|late_event_separator", { dateTime: (0, _DateUtils.formatDate)(mxEv.getDate() ?? new Date()) }); ret.push( /*#__PURE__*/_react.default.createElement("li", { key: ts1 }, /*#__PURE__*/_react.default.createElement(_TimelineSeparator.default, { key: ts1, label: text }, text))); } } const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); let lastInSection = true; if (nextEventWithTile) { const nextEv = nextEventWithTile; const willWantSeparator = this.wantsSeparator(mxEv, nextEv); lastInSection = willWantSeparator === _TimelineSeparator.SeparatorKind.Date || mxEv.getSender() !== nextEv.getSender() || (0, _EventRenderingUtils.getEventDisplayInfo)(cli, nextEv, this.showHiddenEvents).isInfoMessage || !shouldFormContinuation(mxEv, nextEv, cli, this.showHiddenEvents, this.context.timelineRenderingType); } // is this a continuation of the previous message? const continuation = wantsSeparator === _TimelineSeparator.SeparatorKind.None && shouldFormContinuation(prevEvent, mxEv, cli, this.showHiddenEvents, this.context.timelineRenderingType); const eventId = mxEv.getId(); const highlight = eventId === this.props.highlightedEventId; const readReceipts = this.readReceiptsByEvent.get(eventId); const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); // use txnId as key if available so that we don't remount during sending ret.push( /*#__PURE__*/_react.default.createElement(_EventTile.default, { key: mxEv.getTxnId() || eventId, as: "li", ref: this.collectEventTile.bind(this, eventId), alwaysShowTimestamps: this.props.alwaysShowTimestamps, mxEvent: mxEv, continuation: continuation, isRedacted: mxEv.isRedacted(), replacingEventId: mxEv.replacingEventId(), editState: isEditing ? this.props.editState : undefined, onHeightChanged: this.onHeightChanged, readReceipts: readReceipts, readReceiptMap: this.readReceiptMap, showUrlPreview: this.props.showUrlPreview, checkUnmounting: this.isUnmounting, eventSendStatus: mxEv.getAssociatedStatus() ?? undefined, isTwelveHour: this.props.isTwelveHour, permalinkCreator: this.props.permalinkCreator, last: last, lastInSection: lastInSection, lastSuccessful: wrappedEvent.lastSuccessfulWeSent, isSelectedEvent: highlight, getRelationsForEvent: this.props.getRelationsForEvent, showReactions: this.props.showReactions, layout: this.props.layout, showReadReceipts: this.props.showReadReceipts, callEventGrouper: callEventGrouper, hideSender: this.state.hideSender })); return ret; } wantsSeparator(prevEvent, mxEvent) { if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList) { return _TimelineSeparator.SeparatorKind.None; } if (prevEvent !== null) { // If the previous event was late but current is not then show a date separator for orientation // Otherwise if the current event is of a different late group than the previous show a late event separator const lateEventInfo = (0, _LateEventGrouper.getLateEventInfo)(mxEvent); if (lateEventInfo?.group_id !== (0, _LateEventGrouper.getLateEventInfo)(prevEvent)?.group_id) { return lateEventInfo !== undefined ? _TimelineSeparator.SeparatorKind.LateEvent : _TimelineSeparator.SeparatorKind.Date; } } // first event in the panel: depends on if we could back-paginate from here. if (prevEvent === null && !this.props.canBackPaginate) { return _TimelineSeparator.SeparatorKind.Date; } const nextEventDate = mxEvent.getDate() ?? new Date(); if (prevEvent !== null && (0, _DateUtils.wantsDateSeparator)(prevEvent.getDate() || undefined, nextEventDate)) { return _TimelineSeparator.SeparatorKind.Date; } return _TimelineSeparator.SeparatorKind.None; } // 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.safeGet().credentials.userId; // get list of read receipts, sorted most recent first const { room } = this.props; if (!room) { return null; } const receiptDestination = this.context.threadId ? room.getThread(this.context.threadId) : room; const receipts = []; if (!receiptDestination) { _logger.logger.debug("Discarding request, could not find the receiptDestination for event: " + this.context.threadId); return receipts; } receiptDestination.getReceiptsForEvent(event).forEach(r => { if (!r.userId || !(0, _utils.isSupportedReceiptType)(r.type) || r.userId === myUserId) { return; // ignore non-read receipts and receipts from self. } if (_MatrixClientPeg.MatrixClientPeg.safeGet().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(events) { const receiptsByEvent = new Map(); const receiptsByUserId = new Map(); let lastShownEventId; for (const event of this.props.events) { if (this.shouldShowEvent(event)) { lastShownEventId = event.getId(); } if (!lastShownEventId) { continue; } const existingReceipts = receiptsByEvent.get(lastShownEventId) || []; const newReceipts = this.getReadReceiptsForEvent(event); if (!newReceipts) continue; receiptsByEvent.set(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.set(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 of this.readReceiptsByUserId.keys()) { if (receiptsByUserId.get(userId)) { continue; } const { lastShownEventId, receipt } = this.readReceiptsByUserId.get(userId); const existingReceipts = receiptsByEvent.get(lastShownEventId) || []; receiptsByEvent.set(lastShownEventId, existingReceipts.concat(receipt)); receiptsByUserId.set(userId, { lastShownEventId, receipt }); } this.readReceiptsByUserId = receiptsByUserId; // After grouping receipts by shown events, do another pass to sort each // receipt list. for (const receipts of receiptsByEvent.values()) { receipts.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() { let topSpinner; let bottomSpinner; if (this.props.backPaginating) { topSpinner = /*#__PURE__*/_react.default.createElement("li", { key: "_topSpinner" }, /*#__PURE__*/_react.default.createElement(_Spinner.default, null)); } if (this.props.forwardPaginating) { bottomSpinner = /*#__PURE__*/_react.default.createElement("li", { key: "_bottomSpinner" }, /*#__PURE__*/_react.default.createElement(_Spinner.default, null)); } const style = this.props.hidden ? { display: "none" } : {}; let whoIsTyping; if (this.props.room && this.state.showTypingNotifications && this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Room) { whoIsTyping = /*#__PURE__*/_react.default.createElement(_WhoIsTypingTile.default, { room: this.props.room, onShown: this.onTypingShown, onHidden: this.onTypingHidden, ref: this.whoIsTyping }); } let ircResizer; if (this.props.layout == _Layout.Layout.IRC) { ircResizer = /*#__PURE__*/_react.default.createElement(_IRCTimelineProfileResizer.default, { minWidth: 20, maxWidth: 600, roomId: this.props.room?.roomId ?? null }); } const classes = (0, _classnames.default)(this.props.className, { mx_MessagePanel_narrow: this.context.narrow }); return /*#__PURE__*/_react.default.createElement(_ErrorBoundary.default, null, /*#__PURE__*/_react.default.createElement(_ScrollPanel.default, { ref: this.scrollPanel, className: classes, onScroll: this.props.onScroll, 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)); } } /** * Holds on to an event, caching the information about it in the context of the current messages list. * Avoids calling shouldShowEvent more times than we need to. * Simplifies threading of event context like whether it's the last successful event we sent which cannot be determined * by a consumer from the event alone, so has to be done by the event list processing code earlier. */ exports.default = MessagePanel; (0, _defineProperty2.default)(MessagePanel, "contextType", _RoomContext.default); (0, _defineProperty2.default)(MessagePanel, "defaultProps", { disableGrouping: false }); // all the grouper classes that we use, ordered by priority const groupers = [_CreationGrouper.CreationGrouper, _MainGrouper.MainGrouper]; /** * Look through the supplied list of WrappedEvent, and return the first * event that is >start items through the list, and is shown. */ function findFirstShownAfter(start, events) { // Note: this could be done with something like: // events.slice(i + 1).find((e) => e.shouldShow)?.event ?? null; // but it is ~10% slower, and this is on the critical path. for (let n = start + 1; n < events.length; n++) { const { event, shouldShow } = events[n]; if (shouldShow) { return event; } } return null; } //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfcmVhY3QiLCJfaW50ZXJvcFJlcXVpcmVXaWxkY2FyZCIsInJlcXVpcmUiLCJfcmVhY3REb20iLCJfaW50ZXJvcFJlcXVpcmVEZWZhdWx0IiwiX2NsYXNzbmFtZXMiLCJfbWF0cml4IiwiX2xvZ2dlciIsIl91dGlscyIsIl9zaG91bGRIaWRlRXZlbnQiLCJfRGF0ZVV0aWxzIiwiX01hdHJpeENsaWVudFBlZyIsIl9TZXR0aW5nc1N0b3JlIiwiX1Jvb21Db250ZXh0IiwiX0xheW91dCIsIl9FdmVudFRpbGUiLCJfSVJDVGltZWxpbmVQcm9maWxlUmVzaXplciIsIl9kaXNwYXRjaGVyIiwiX1dob0lzVHlwaW5nVGlsZSIsIl9TY3JvbGxQYW5lbCIsIl9EYXRlU2VwYXJhdG9yIiwiX1RpbWVsaW5lU2VwYXJhdG9yIiwiX0Vycm9yQm91bmRhcnkiLCJfU3Bpbm5lciIsIl9hY3Rpb25zIiwiX0V2ZW50UmVuZGVyaW5nVXRpbHMiLCJfRXZlbnRUaWxlRmFjdG9yeSIsIl9FZGl0aW5nIiwiX0V2ZW50VXRpbHMiLCJfTWFpbkdyb3VwZXIiLCJfQ3JlYXRpb25Hcm91cGVyIiwiX2xhbmd1YWdlSGFuZGxlciIsIl9MYXRlRXZlbnRHcm91cGVyIiwiX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlIiwiZSIsIldlYWtNYXAiLCJyIiwidCIsIl9fZXNNb2R1bGUiLCJkZWZhdWx0IiwiaGFzIiwiZ2V0IiwibiIsIl9fcHJvdG9fXyIsImEiLCJPYmplY3QiLCJkZWZpbmVQcm9wZXJ0eSIsImdldE93blByb3BlcnR5RGVzY3JpcHRvciIsInUiLCJoYXNPd25Qcm9wZXJ0eSIsImNhbGwiLCJpIiwic2V0IiwiQ09OVElOVUFUSU9OX01BWF9JTlRFUlZBTCIsImNvbnRpbnVlZFR5cGVzIiwiRXZlbnRUeXBlIiwiU3RpY2tlciIsIlJvb21NZXNzYWdlIiwic2hvdWxkRm9ybUNvbnRpbnVhdGlvbiIsInByZXZFdmVudCIsIm14RXZlbnQiLCJtYXRyaXhDbGllbnQiLCJzaG93SGlkZGVuRXZlbnRzIiwidGltZWxpbmVSZW5kZXJpbmdUeXBlIiwiVGltZWxpbmVSZW5kZXJpbmdUeXBlIiwiVGhyZWFkc0xpc3QiLCJzZW5kZXIiLCJnZXRUcyIsImlzUmVkYWN0ZWQiLCJnZXRUeXBlIiwiaW5jbHVkZXMiLCJ1c2VySWQiLCJuYW1lIiwiZ2V0TXhjQXZhdGFyVXJsIiwiaGFzVGhyZWFkU3VtbWFyeSIsIlRocmVhZCIsImhhdmVSZW5kZXJlckZvckV2ZW50IiwiTWVzc2FnZVBhbmVsIiwiUmVhY3QiLCJDb21wb25lbnQiLCJjb25zdHJ1Y3RvciIsInByb3BzIiwiY29udGV4dCIsIl9kZWZpbmVQcm9wZXJ0eTIiLCJNYXAiLCJjcmVhdGVSZWYiLCJzZXRTdGF0ZSIsImhpZGVTZW5kZXIiLCJzaG91bGRIaWRlU2VuZGVyIiwic2hvd1R5cGluZ05vdGlmaWNhdGlvbnMiLCJTZXR0aW5nc1N0b3JlIiwiZ2V0VmFsdWUiLCJpc01vdW50ZWQiLCJub2RlIiwicmVxdWVzdEFuaW1hdGlvbkZyYW1lIiwic3R5bGUiLCJ3aWR0aCIsIm9wYWNpdHkiLCJldiIsImZpbmlzaGVkRXZlbnRJZCIsInRhcmdldCIsImRhdGFzZXQiLCJldmVudGlkIiwiZ2hvc3RSZWFkTWFya2VycyIsInN0YXRlIiwiZmlsdGVyIiwiZWlkIiwiZXZlbnRJZCIsImV2ZW50VGlsZXMiLCJzY3JvbGxQYW5lbCIsImN1cnJlbnQiLCJjaGVja1Njcm9sbCIsImdldFNjcm9sbFN0YXRlIiwic3R1Y2tBdEJvdHRvbSIsInByZXZlbnRTaHJpbmtpbmciLCJ1cGRhdGVQcmV2ZW50U2hyaW5raW5nIiwiX3Nob3dIaWRkZW5FdmVudHMiLCJzaG93VHlwaW5nTm90aWZpY2F0aW9uc1dhdGNoZXJSZWYiLCJ3YXRjaFNldHRpbmciLCJvblNob3dUeXBpbmdOb3RpZmljYXRpb25zQ2hhbmdlIiwiY29tcG9uZW50RGlkTW91bnQiLCJjYWxjdWxhdGVSb29tTWVtYmVyc0NvdW50Iiwicm9vbSIsImN1cnJlbnRTdGF0ZSIsIm9uIiwiUm9vbVN0YXRlRXZlbnQiLCJVcGRhdGUiLCJjb21wb25lbnRXaWxsVW5tb3VudCIsIm9mZiIsInVud2F0Y2hTZXR0aW5nIiwicmVhZFJlY2VpcHRNYXAiLCJjb21wb25lbnREaWRVcGRhdGUiLCJwcmV2UHJvcHMiLCJwcmV2U3RhdGUiLCJsYXlvdXQiLCJyZWFkTWFya2VyVmlzaWJsZSIsInJlYWRNYXJrZXJFdmVudElkIiwicHVzaCIsInBlbmRpbmdFZGl0SXRlbSIsImVkaXRTdGF0ZSIsImV2ZW50IiwiZmluZEV2ZW50QnlJZCIsImRlZmF1bHREaXNwYXRjaGVyIiwiZGlzcGF0Y2giLCJhY3Rpb24iLCJBY3Rpb24iLCJFZGl0RXZlbnQiLCJnZXRJbnZpdGVkQW5kSm9pbmVkTWVtYmVyQ291bnQiLCJMYXlvdXQiLCJCdWJibGUiLCJnZXROb2RlRm9yRXZlbnRJZCIsInVuZGVmaW5lZCIsInJlZiIsImdldFRpbGVGb3JFdmVudElkIiwiaXNBdEJvdHRvbSIsImdldFJlYWRNYXJrZXJQb3NpdGlvbiIsInJlYWRNYXJrZXIiLCJyZWFkTWFya2VyTm9kZSIsIm1lc3NhZ2VXcmFwcGVyIiwid3JhcHBlclJlY3QiLCJSZWFjdERPTSIsImZpbmRET01Ob2RlIiwiZ2V0Qm91bmRpbmdDbGllbnRSZWN0IiwicmVhZE1hcmtlclJlY3QiLCJib3R0b20iLCJ0b3AiLCJzY3JvbGxUb1RvcCIsInNjcm9sbFRvQm90dG9tIiwiaGFuZGxlU2Nyb2xsS2V5Iiwic2Nyb2xsVG9FdmVudCIsInBpeGVsT2Zmc2V0Iiwib2Zmc2V0QmFzZSIsInNjcm9sbFRvVG9rZW4iLCJzY3JvbGxUb0V2ZW50SWZOZWVkZWQiLCJzY3JvbGxJbnRvVmlldyIsImJsb2NrIiwiYmVoYXZpb3IiLCJzaG91bGRTaG93RXZlbnQiLCJteEV2IiwiZm9yY2VIaWRlRXZlbnRzIiwiaGlkZVRocmVhZGVkTWVzc2FnZXMiLCJzaG91bGRMaXZlSW5Sb29tIiwiZXZlbnRTaG91bGRMaXZlSW4iLCJldmVudHMiLCJNYXRyaXhDbGllbnRQZWciLCJzYWZlR2V0IiwiaXNVc2VySWdub3JlZCIsImdldFNlbmRlciIsImhpZ2hsaWdodGVkRXZlbnRJZCIsImdldElkIiwic2hvdWxkSGlkZUV2ZW50IiwicmVhZE1hcmtlckZvckV2ZW50IiwiaXNMYXN0RXZlbnQiLCJGaWxlIiwidmlzaWJsZSIsImhyIiwiY3JlYXRlRWxlbWVudCIsImtleSIsImNsYXNzTmFtZSIsImNvbGxlY3RHaG9zdFJlYWRNYXJrZXIiLCJvblRyYW5zaXRpb25FbmQiLCJvbkdob3N0VHJhbnNpdGlvbkVuZCIsImdldE5leHRFdmVudEluZm8iLCJuZXh0RXZlbnRBbmRTaG91bGRTaG93IiwibGVuZ3RoIiwibmV4dFRpbGUiLCJmaW5kRmlyc3RTaG93bkFmdGVyIiwibG9jYWxTdG9yYWdlIiwiZ2V0SXRlbSIsImVkaXRvclJvb21LZXkiLCJyb29tSWQiLCJlcnIiLCJsb2dnZXIiLCJlcnJvciIsImlzU2VudFN0YXRlIiwic3RhdHVzIiwiZ2V0QXNzb2NpYXRlZFN0YXR1cyIsIkV2ZW50U3RhdHVzIiwiU0VOVCIsImdldEV2ZW50VGlsZXMiLCJsYXN0U2hvd25FdmVudCIsIm1hcCIsInNob3VsZFNob3ciLCJnZXRTYWZlVXNlcklkIiwiZm91bmRMYXN0U3VjY2Vzc2Z1bEV2ZW50IiwibGFzdFNob3duTm9uTG9jYWxFY2hvSW5kZXgiLCJpc0VsaWdpYmxlRm9yU3BlY2lhbFJlY2VpcHQiLCJsYXN0U3VjY2Vzc2Z1bFdlU2VudCIsInJldCIsInJlYWRSZWNlaXB0c0J5RXZlbnQiLCJzaG93UmVhZFJlY2VpcHRzIiwiZ2V0UmVhZFJlY2VpcHRzQnlTaG93bkV2ZW50IiwiZ3JvdXBlciIsIndyYXBwZWRFdmVudCIsImxhc3QiLCJzaG91bGRHcm91cCIsImFkZCIsImdldFRpbGVzIiwiZ2V0TmV3UHJldkV2ZW50IiwiR3JvdXBlciIsImdyb3VwZXJzIiwiY2FuU3RhcnRHcm91cCIsImRpc2FibGVHcm91cGluZyIsImdldFRpbGVzRm9yRXZlbnQiLCJpc0dyb3VwZWQiLCJuZXh0RXZlbnQiLCJuZXh0RXZlbnRXaXRoVGlsZSIsImlzRWRpdGluZyIsImdldEV2ZW50IiwidHMxIiwiRGF0ZSIsIm5vdyIsIndhbnRzU2VwYXJhdG9yIiwiU2VwYXJhdG9yS2luZCIsInRzIiwiTGF0ZUV2ZW50IiwidGV4dCIsIl90IiwiZGF0ZVRpbWUiLCJmb3JtYXREYXRlIiwiZ2V0RGF0ZSIsImxhYmVsIiwiY2xpIiwibGFzdEluU2VjdGlvbiIsIm5leHRFdiIsIndpbGxXYW50U2VwYXJhdG9yIiwiZ2V0RXZlbnREaXNwbGF5SW5mbyIsImlzSW5mb01lc3NhZ2UiLCJjb250aW51YXRpb24iLCJOb25lIiwiaGlnaGxpZ2h0IiwicmVhZFJlY2VpcHRzIiwiY2FsbEV2ZW50R3JvdXBlciIsImNhbGxFdmVudEdyb3VwZXJzIiwiZ2V0Q29udGVudCIsImNhbGxfaWQiLCJnZXRUeG5JZCIsImFzIiwiY29sbGVjdEV2ZW50VGlsZSIsImJpbmQiLCJhbHdheXNTaG93VGltZXN0YW1wcyIsInJlcGxhY2luZ0V2ZW50SWQiLCJvbkhlaWdodENoYW5nZWQiLCJzaG93VXJsUHJldmlldyIsImNoZWNrVW5tb3VudGluZyIsImlzVW5tb3VudGluZyIsImV2ZW50U2VuZFN0YXR1cyIsImlzVHdlbHZlSG91ciIsInBlcm1hbGlua0NyZWF0b3IiLCJsYXN0U3VjY2Vzc2Z1bCIsImlzU2VsZWN0ZWRFdmVudCIsImdldFJlbGF0aW9uc0ZvckV2ZW50Iiwic2hvd1JlYWN0aW9ucyIsImxhdGVFdmVudEluZm8iLCJnZXRMYXRlRXZlbnRJbmZvIiwiZ3JvdXBfaWQiLCJjYW5CYWNrUGFnaW5hdGUiLCJuZXh0RXZlbnREYXRlIiwid2FudHNEYXRlU2VwYXJhdG9yIiwiZ2V0UmVhZFJlY2VpcHRzRm9yRXZlbnQiLCJteVVzZXJJZCIsImNyZWRlbnRpYWxzIiwicmVjZWlwdERlc3RpbmF0aW9uIiwidGhyZWFkSWQiLCJnZXRUaHJlYWQiLCJyZWNlaXB0cyIsImRlYnVnIiwiZ2V0UmVjZWlwdHNGb3JFdmVudCIsImZvckVhY2giLCJpc1N1cHBvcnRlZFJlY2VpcHRUeXBlIiwidHlwZSIsIm1lbWJlciIsImdldE1lbWJlciIsInJvb21NZW1iZXIiLCJkYXRhIiwicmVjZWlwdHNCeUV2ZW50IiwicmVjZWlwdHNCeVVzZXJJZCIsImxhc3RTaG93bkV2ZW50SWQiLCJleGlzdGluZ1JlY2VpcHRzIiwibmV3UmVjZWlwdHMiLCJjb25jYXQiLCJyZWNlaXB0IiwicmVhZFJlY2VpcHRzQnlVc2VySWQiLCJrZXlzIiwidmFsdWVzIiwic29ydCIsInIxIiwicjIiLCJ1cGRhdGVUaW1lbGluZU1pbkhlaWdodCIsIndob0lzVHlwaW5nIiwiaXNUeXBpbmdWaXNpYmxlIiwiaXNWaXNpYmxlIiwib25UaW1lbGluZVJlc2V0IiwiY2xlYXJQcmV2ZW50U2hyaW5raW5nIiwicmVuZGVyIiwidG9wU3Bpbm5lciIsImJvdHRvbVNwaW5uZXIiLCJiYWNrUGFnaW5hdGluZyIsImZvcndhcmRQYWdpbmF0aW5nIiwiaGlkZGVuIiwiZGlzcGxheSIsIlJvb20iLCJvblNob3duIiwib25UeXBpbmdTaG93biIsIm9uSGlkZGVuIiwib25UeXBpbmdIaWRkZW4iLCJpcmNSZXNpemVyIiwiSVJDIiwibWluV2lkdGgiLCJtYXhXaWR0aCIsImNsYXNzZXMiLCJjbGFzc05hbWVzIiwibXhfTWVzc2FnZVBhbmVsX25hcnJvdyIsIm5hcnJvdyIsIm9uU2Nyb2xsIiwib25GaWxsUmVxdWVzdCIsIm9uVW5maWxsUmVxdWVzdCIsInN0aWNreUJvdHRvbSIsInJlc2l6ZU5vdGlmaWVyIiwiZml4ZWRDaGlsZHJlbiIsImV4cG9ydHMiLCJSb29tQ29udGV4dCIsIkNyZWF0aW9uR3JvdXBlciIsIk1haW5Hcm91cGVyIiwic3RhcnQiXSwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29tcG9uZW50cy9zdHJ1Y3R1cmVzL01lc3NhZ2VQYW5lbC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiLypcbkNvcHlyaWdodCAyMDI0IE5ldyBWZWN0b3IgTHRkLlxuQ29weXJpZ2h0IDIwMTYtMjAyMyBUaGUgTWF0cml4Lm9yZyBGb3VuZGF0aW9uIEMuSS5DLlxuXG5TUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQUdQTC0zLjAtb25seSBPUiBHUEwtMy4wLW9ubHlcblBsZWFzZSBzZWUgTElDRU5TRSBmaWxlcyBpbiB0aGUgcmVwb3NpdG9yeSByb290IGZvciBmdWxsIGRldGFpbHMuXG4qL1xuXG5pbXBvcnQgUmVhY3QsIHsgY3JlYXRlUmVmLCBSZWFjdE5vZGUsIFRyYW5zaXRpb25FdmVudCB9IGZyb20gXCJyZWFjdFwiO1xuaW1wb3J0IFJlYWN0RE9NIGZyb20gXCJyZWFjdC1kb21cIjtcbmltcG9ydCBjbGFzc05hbWVzIGZyb20gXCJjbGFzc25hbWVzXCI7XG5pbXBvcnQgeyBSb29tLCBNYXRyaXhDbGllbnQsIFJvb21TdGF0ZUV2ZW50LCBFdmVudFN0YXR1cywgTWF0cml4RXZlbnQsIEV2ZW50VHlwZSB9IGZyb20gXCJtYXRyaXgtanMtc2RrL3NyYy9tYXRyaXhcIjtcbmltcG9ydCB7IGxvZ2dlciB9IGZyb20gXCJtYXRyaXgtanMtc2RrL3NyYy9sb2dnZXJcIjtcbmltcG9ydCB7IGlzU3VwcG9ydGVkUmVjZWlwdFR5cGUgfSBmcm9tIFwibWF0cml4LWpzLXNkay9zcmMvdXRpbHNcIjtcblxuaW1wb3J0IHNob3VsZEhpZGVFdmVudCBmcm9tIFwiLi4vLi4vc2hvdWxkSGlkZUV2ZW50XCI7XG5pbXBvcnQgeyBmb3JtYXREYXRlLCB3YW50c0RhdGVTZXBhcmF0b3IgfSBmcm9tIFwiLi4vLi4vRGF0ZVV0aWxzXCI7XG5pbXBvcnQgeyBNYXRyaXhDbGllbnRQZWcgfSBmcm9tIFwiLi4vLi4vTWF0cml4Q2xpZW50UGVnXCI7XG5pbXBvcnQgU2V0dGluZ3NTdG9yZSBmcm9tIFwiLi4vLi4vc2V0dGluZ3MvU2V0dGluZ3NTdG9yZVwiO1xuaW1wb3J0IFJvb21Db250ZXh0LCB7IFRpbWVsaW5lUmVuZGVyaW5nVHlwZSB9IGZyb20gXCIuLi8uLi9jb250ZXh0cy9Sb29tQ29udGV4dFwiO1xuaW1wb3J0IHsgTGF5b3V0IH0gZnJvbSBcIi4uLy4uL3NldHRpbmdzL2VudW1zL0xheW91dFwiO1xuaW1wb3J0IEV2ZW50VGlsZSwge1xuICAgIEdldFJlbGF0aW9uc0ZvckV2ZW50LFxuICAgIElSZWFkUmVjZWlwdFByb3BzLFxuICAgIGlzRWxpZ2libGVGb3JTcGVjaWFsUmVjZWlwdCxcbiAgICBVbndyYXBwZWRFdmVudFRpbGUsXG59IGZyb20gXCIuLi92aWV3cy9yb29tcy9FdmVudFRpbGVcIjtcbmltcG9ydCBJUkNUaW1lbGluZVByb2ZpbGVSZXNpemVyIGZyb20gXCIuLi92aWV3cy9lbGVtZW50cy9JUkNUaW1lbGluZVByb2ZpbGVSZXNpemVyXCI7XG5pbXBvcnQgZGVmYXVsdERpc3BhdGNoZXIgZnJvbSBcIi4uLy4uL2Rpc3BhdGNoZXIvZGlzcGF0Y2hlclwiO1xuaW1wb3J0IExlZ2FjeUNhbGxFdmVudEdyb3VwZXIgZnJvbSBcIi4vTGVnYWN5Q2FsbEV2ZW50R3JvdXBlclwiO1xuaW1wb3J0IFdob0lzVHlwaW5nVGlsZSBmcm9tIFwiLi4vdmlld3Mvcm9vbXMvV2hvSXNUeXBpbmdUaWxlXCI7XG5pbXBvcnQgU2Nyb2xsUGFuZWwsIHsgSVNjcm9sbFN0YXRlIH0gZnJvbSBcIi4vU2Nyb2xsUGFuZWxcIjtcbmltcG9ydCBEYXRlU2VwYXJhdG9yIGZyb20gXCIuLi92aWV3cy9tZXNzYWdlcy9EYXRlU2VwYXJhdG9yXCI7XG5pbXBvcnQgVGltZWxpbmVTZXBhcmF0b3IsIHsgU2VwYXJhdG9yS2luZCB9IGZyb20gXCIuLi92aWV3cy9tZXNzYWdlcy9UaW1lbGluZVNlcGFyYXRvclwiO1xuaW1wb3J0IEVycm9yQm91bmRhcnkgZnJvbSBcIi4uL3ZpZXdzL2VsZW1lbnRzL0Vycm9yQm91bmRhcnlcIjtcbmltcG9ydCBSZXNpemVOb3RpZmllciBmcm9tIFwiLi4vLi4vdXRpbHMvUmVzaXplTm90aWZpZXJcIjtcbmltcG9ydCBTcGlubmVyIGZyb20gXCIuLi92aWV3cy9lbGVtZW50cy9TcGlubmVyXCI7XG5pbXBvcnQgeyBSb29tUGVybWFsaW5rQ3JlYXRvciB9IGZyb20gXCIuLi8uLi91dGlscy9wZXJtYWxpbmtzL1Blcm1hbGlua3NcIjtcbmltcG9ydCBFZGl0b3JTdGF0ZVRyYW5zZmVyIGZyb20gXCIuLi8uLi91dGlscy9FZGl0b3JTdGF0ZVRyYW5zZmVyXCI7XG5pbXBvcnQgeyBBY3Rpb24gfSBmcm9tIFwiLi4vLi4vZGlzcGF0Y2hlci9hY3Rpb25zXCI7XG5pbXBvcnQgeyBnZXRFdmVudERpc3BsYXlJbmZvIH0gZnJvbSBcIi4uLy4uL3V0aWxzL0V2ZW50UmVuZGVyaW5nVXRpbHNcIjtcbmltcG9ydCB7IElSZWFkUmVjZWlwdFBvc2l0aW9uIH0gZnJvbSBcIi4uL3ZpZXdzL3Jvb21zL1JlYWRSZWNlaXB0TWFya2VyXCI7XG5pbXBvcnQgeyBoYXZlUmVuZGVyZXJGb3JFdmVudCB9IGZyb20gXCIuLi8uLi9ldmVudHMvRXZlbnRUaWxlRmFjdG9yeVwiO1xuaW1wb3J0IHsgZWRpdG9yUm9vbUtleSB9IGZyb20gXCIuLi8uLi9FZGl0aW5nXCI7XG5pbXBvcnQgeyBoYXNUaHJlYWRTdW1tYXJ5IH0gZnJvbSBcIi4uLy4uL3V0aWxzL0V2ZW50VXRpbHNcIjtcbmltcG9ydCB7IEJhc2VHcm91cGVyIH0gZnJvbSBcIi4vZ3JvdXBlci9CYXNlR3JvdXBlclwiO1xuaW1wb3J0IHsgTWFpbkdyb3VwZXIgfSBmcm9tIFwiLi9ncm91cGVyL01haW5Hcm91cGVyXCI7XG5pbXBvcnQgeyBDcmVhdGlvbkdyb3VwZXIgfSBmcm9tIFwiLi9ncm91cGVyL0NyZWF0aW9uR3JvdXBlclwiO1xuaW1wb3J0IHsgX3QgfSBmcm9tIFwiLi4vLi4vbGFuZ3VhZ2VIYW5kbGVyXCI7XG5pbXBvcnQgeyBnZXRMYXRlRXZlbnRJbmZvIH0gZnJvbSBcIi4vZ3JvdXBlci9MYXRlRXZlbnRHcm91cGVyXCI7XG5cbmNvbnN0IENPTlRJTlVBVElPTl9NQVhfSU5URVJWQUwgPSA1ICogNjAgKiAxMDAwOyAvLyA1IG1pbnV0ZXNcbmNvbnN0IGNvbnRpbnVlZFR5cGVzID0gW0V2ZW50VHlwZS5TdGlja2VyLCBFdmVudFR5cGUuUm9vbU1lc3NhZ2VdO1xuXG4vLyBjaGVjayBpZiB0aGVyZSBpcyBhIHByZXZpb3VzIGV2ZW50IGFuZCBpdCBoYXMgdGhlIHNhbWUgc2VuZGVyIGFzIHRoaXMgZXZlbnRcbi8vIGFuZCB0aGUgdHlwZXMgYXJlIHRoZSBzYW1lL2lzIGluIGNvbnRpbnVlZFR5cGVzIGFuZCB0aGUgdGltZSBiZXR3ZWVuIHRoZW0gaXMgPD0gQ09OVElOVUFUSU9OX01BWF9JTlRFUlZBTFxuZXhwb3J0IGZ1bmN0aW9uIHNob3VsZEZvcm1Db250aW51YXRpb24oXG4gICAgcHJldkV2ZW50OiBNYXRyaXhFdmVudCB8IG51bGwsXG4gICAgbXhFdmVudDogTWF0cml4RXZlbnQsXG4gICAgbWF0cml4Q2xpZW50OiBNYXRyaXhDbGllbnQsXG4gICAgc2hvd0hpZGRlbkV2ZW50czogYm9vbGVhbixcbiAgICB0aW1lbGluZVJlbmRlcmluZ1R5cGU/OiBUaW1lbGluZVJlbmRlcmluZ1R5cGUsXG4pOiBib29sZWFuIHtcbiAgICBpZiAodGltZWxpbmVSZW5kZXJpbmdUeXBlID09PSBUaW1lbGluZVJlbmRlcmluZ1R5cGUuVGhyZWFkc0xpc3QpIHJldHVybiBmYWxzZTtcbiAgICAvLyBzYW5pdHkgY2hlY2sgaW5wdXRzXG4gICAgaWYgKCFwcmV2RXZlbnQ/LnNlbmRlciB8fCAhbXhFdmVudC5zZW5kZXIpIHJldHVybiBmYWxzZTtcbiAgICAvLyBjaGVjayBpZiB3aXRoaW4gdGhlIG1heCBjb250aW51YXRpb24gcGVyaW9kXG4gICAgaWYgKG14RXZlbnQuZ2V0VHMoKSAtIHByZXZFdmVudC5nZXRUcygpID4gQ09OVElOVUFUSU9OX01BWF9JTlRFUlZBTCkgcmV0dXJuIGZhbHNlO1xuXG4gICAgLy8gQXMgd2Ugc3VtbWFyaXNlIHJlZGFjdGlvbnMsIGRvIG5vdCBjb250aW51ZSBhIHJlZGFjdGVkIGV2ZW50IG9udG8gYSBub24tcmVkYWN0ZWQgb25lIGFuZCB2aWNlLXZlcnNhXG4gICAgaWYgKG14RXZlbnQuaXNSZWRhY3RlZCgpICE9PSBwcmV2RXZlbnQuaXNSZWRhY3RlZCgpKSByZXR1cm4gZmFsc2U7XG5cbiAgICAvLyBTb21lIGV2ZW50cyBzaG91bGQgYXBwZWFyIGFzIGNvbnRpbnVhdGlvbnMgZnJvbSBwcmV2aW91cyBldmVudHMgb2YgZGlmZmVyZW50IHR5cGVzLlxuICAgIGlmIChcbiAgICAgICAgbXhFdmVudC5nZXRUeXBlKCkgIT09IHByZXZFdmVudC5nZXRUeXBlKCkgJiZcbiAgICAgICAgKCFjb250aW51ZWRUeXBlcy5pbmNsdWRlcyhteEV2ZW50LmdldFR5cGUoKSBhcyBFdmVudFR5cGUpIHx8XG4gICAgICAgICAgICAhY29udGludWVkVHlwZXMuaW5jbHVkZXMocHJldkV2ZW50LmdldFR5cGUoKSBhcyBFdmVudFR5cGUpKVxuICAgIClcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuXG4gICAgLy8gQ2hlY2sgaWYgdGhlIHNlbmRlciBpcyB0aGUgc2FtZSBhbmQgaGFzbid0IGNoYW5nZWQgdGhlaXIgZGlzcGxheW5hbWUvYXZhdGFyIGJldHdlZW4gdGhlc2UgZXZlbnRzXG4gICAgaWYgKFxuICAgICAgICBteEV2ZW50LnNlbmRlci51c2VySWQgIT09IHByZXZFdmVudC5zZW5kZXIudXNlcklkIHx8XG4gICAgICAgIG14RXZlbnQuc2VuZGVyLm5hbWUgIT09IHByZXZFdmVudC5zZW5kZXIubmFtZSB8fFxuICAgICAgICBteEV2ZW50LnNlbmRlci5nZXRNeGNBdmF0YXJVcmwoKSAhPT0gcHJldkV2ZW50LnNlbmRlci5nZXRNeGNBdmF0YXJVcmwoKVxuICAgIClcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuXG4gICAgLy8gVGhyZWFkIHN1bW1hcmllcyBpbiB0aGUgbWFpbiB0aW1lbGluZSBzaG91bGQgYnJlYWsgdXAgYSBjb250