matrix-react-sdk
Version:
SDK for matrix.org using React
859 lines (824 loc) • 136 kB
JavaScript
"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