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