matrix-react-sdk
Version:
SDK for matrix.org using React
1,370 lines (1,165 loc) • 273 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 _classnames = _interopRequireDefault(require("classnames"));
var _shouldHideEvent = _interopRequireDefault(require("../../shouldHideEvent"));
var _languageHandler = require("../../languageHandler");
var _Permalinks = require("../../utils/permalinks/Permalinks");
var _ContentMessages = _interopRequireDefault(require("../../ContentMessages"));
var _Modal = _interopRequireDefault(require("../../Modal"));
var sdk = _interopRequireWildcard(require("../../index"));
var _CallHandler = _interopRequireDefault(require("../../CallHandler"));
var _dispatcher = _interopRequireDefault(require("../../dispatcher/dispatcher"));
var _Tinter = _interopRequireDefault(require("../../Tinter"));
var _ratelimitedfunc = _interopRequireDefault(require("../../ratelimitedfunc"));
var Rooms = _interopRequireWildcard(require("../../Rooms"));
var _Searching = _interopRequireWildcard(require("../../Searching"));
var _MainSplit = _interopRequireDefault(require("./MainSplit"));
var _RightPanel = _interopRequireDefault(require("./RightPanel"));
var _RoomViewStore = _interopRequireDefault(require("../../stores/RoomViewStore"));
var _RoomScrollStateStore = _interopRequireDefault(require("../../stores/RoomScrollStateStore"));
var _WidgetEchoStore = _interopRequireDefault(require("../../stores/WidgetEchoStore"));
var _SettingsStore = _interopRequireDefault(require("../../settings/SettingsStore"));
var _Layout = require("../../settings/Layout");
var _AccessibleButton = _interopRequireDefault(require("../views/elements/AccessibleButton"));
var _RightPanelStore = _interopRequireDefault(require("../../stores/RightPanelStore"));
var _EventTile = require("../views/rooms/EventTile");
var _RoomContext = _interopRequireDefault(require("../../contexts/RoomContext"));
var _MatrixClientContext = _interopRequireDefault(require("../../contexts/MatrixClientContext"));
var _ShieldUtils = require("../../utils/ShieldUtils");
var _actions = require("../../dispatcher/actions");
var _SettingLevel = require("../../settings/SettingLevel");
var _ScrollPanel = _interopRequireDefault(require("./ScrollPanel"));
var _TimelinePanel = _interopRequireDefault(require("./TimelinePanel"));
var _ErrorBoundary = _interopRequireDefault(require("../views/elements/ErrorBoundary"));
var _RoomPreviewBar = _interopRequireDefault(require("../views/rooms/RoomPreviewBar"));
var _ForwardMessage = _interopRequireDefault(require("../views/rooms/ForwardMessage"));
var _SearchBar = _interopRequireDefault(require("../views/rooms/SearchBar"));
var _RoomUpgradeWarningBar = _interopRequireDefault(require("../views/rooms/RoomUpgradeWarningBar"));
var _PinnedEventsPanel = _interopRequireDefault(require("../views/rooms/PinnedEventsPanel"));
var _AuxPanel = _interopRequireDefault(require("../views/rooms/AuxPanel"));
var _RoomHeader = _interopRequireDefault(require("../views/rooms/RoomHeader"));
var _EffectsOverlay = _interopRequireDefault(require("../views/elements/EffectsOverlay"));
var _utils = require("../../effects/utils");
var _effects = require("../../effects");
var _WidgetStore = _interopRequireDefault(require("../../stores/WidgetStore"));
var _AsyncStore = require("../../stores/AsyncStore");
var _Notifier = _interopRequireDefault(require("../../Notifier"));
var _DesktopNotificationsToast = require("../../toasts/DesktopNotificationsToast");
var _RoomNotificationStateStore = require("../../stores/notifications/RoomNotificationStateStore");
var _WidgetLayoutStore = require("../../stores/widgets/WidgetLayoutStore");
var _KeyBindingsManager = require("../../KeyBindingsManager");
var _objects = require("../../utils/objects");
var _SpaceRoomView = _interopRequireDefault(require("./SpaceRoomView"));
var _replaceableComponent = require("../../utils/replaceableComponent");
var _dec, _class, _class2, _temp;
const DEBUG = false;
let debuglog = function (msg
/*: string*/
) {};
const BROWSER_SUPPORTS_SANDBOX = ('sandbox' in document.createElement('iframe'));
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console);
}
/*:: export interface IState {
room?: Room;
roomId?: string;
roomAlias?: string;
roomLoading: boolean;
peekLoading: boolean;
shouldPeek: boolean;
// used to trigger a rerender in TimelinePanel once the members are loaded,
// so RR are rendered again (now with the members available), ...
membersLoaded: boolean;
// The event to be scrolled to initially
initialEventId?: string;
// The offset in pixels from the event with which to scroll vertically
initialEventPixelOffset?: number;
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent;
numUnreadMessages: number;
draggingFile: boolean;
searching: boolean;
searchTerm?: string;
searchScope?: "All" | "Room";
searchResults?: XOR<{}, {
count: number;
highlights: string[];
results: MatrixEvent[];
next_batch: string; // eslint-disable-line camelcase
}>;
searchHighlights?: string[];
searchInProgress?: boolean;
callState?: CallState;
guestsCanJoin: boolean;
canPeek: boolean;
showApps: boolean;
isPeeking: boolean;
showingPinned: boolean;
showReadReceipts: boolean;
showRightPanel: boolean;
// error object, as from the matrix client/server API
// If we failed to load information about the room,
// store the error here.
roomLoadError?: Error;
// Have we sent a request to join the room that we're waiting to complete?
joining: boolean;
// this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline. It has the effect of hiding the
// 'scroll to bottom' knob, among a couple of other things.
atEndOfLiveTimeline: boolean;
// used by componentDidUpdate to avoid unnecessary checks
atEndOfLiveTimelineInit: boolean;
showTopUnreadMessagesBar: boolean;
auxPanelMaxHeight?: number;
statusBarVisible: boolean;
// We load this later by asking the js-sdk to suggest a version for us.
// This object is the result of Room#getRecommendedVersion()
upgradeRecommendation?: {
version: string;
needsUpgrade: boolean;
urgent: boolean;
};
canReact: boolean;
canReply: boolean;
layout: Layout;
matrixClientIsReady: boolean;
showUrlPreview?: boolean;
e2eStatus?: E2EStatus;
rejecting?: boolean;
rejectError?: Error;
hasPinnedWidgets?: boolean;
dragCounter: number;
// whether or not a spaces context switch brought us here,
// if it did we don't want the room to be marked as read as soon as it is loaded.
wasContextSwitch?: boolean;
}*/
let RoomView = (_dec = (0, _replaceableComponent.replaceableComponent)("structures.RoomView"), _dec(_class = (_temp = _class2 = class RoomView extends _react.default.Component
/*:: <IProps, IState>*/
{
constructor(props, context) {
super(props, context);
(0, _defineProperty2.default)(this, "dispatcherRef", void 0);
(0, _defineProperty2.default)(this, "roomStoreToken", void 0);
(0, _defineProperty2.default)(this, "rightPanelStoreToken", void 0);
(0, _defineProperty2.default)(this, "showReadReceiptsWatchRef", void 0);
(0, _defineProperty2.default)(this, "layoutWatcherRef", void 0);
(0, _defineProperty2.default)(this, "unmounted", false);
(0, _defineProperty2.default)(this, "permalinkCreators", {});
(0, _defineProperty2.default)(this, "searchId", void 0);
(0, _defineProperty2.default)(this, "roomView", /*#__PURE__*/(0, _react.createRef)());
(0, _defineProperty2.default)(this, "searchResultsPanel", /*#__PURE__*/(0, _react.createRef)());
(0, _defineProperty2.default)(this, "messagePanel", void 0);
(0, _defineProperty2.default)(this, "onWidgetStoreUpdate", () => {
if (this.state.room) {
this.checkWidgets(this.state.room);
}
});
(0, _defineProperty2.default)(this, "checkWidgets", room => {
this.setState({
hasPinnedWidgets: _WidgetLayoutStore.WidgetLayoutStore.instance.getContainerWidgets(room, _WidgetLayoutStore.Container.Top).length > 0,
showApps: this.shouldShowApps(room)
});
});
(0, _defineProperty2.default)(this, "onReadReceiptsChange", () => {
this.setState({
showReadReceipts: _SettingsStore.default.getValue("showReadReceipts", this.state.roomId)
});
});
(0, _defineProperty2.default)(this, "onRoomViewStoreUpdate", (initial
/*: boolean*/
) => {
if (this.unmounted) {
return;
}
if (!initial && this.state.roomId !== _RoomViewStore.default.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// switching rooms. Therefore, if the room ID changes, we
// ignore this. We either need to do this or add code to handle
// saving the scroll position (otherwise we end up saving the
// scroll position against the wrong room).
// Given that doing the setState here would cause a bunch of
// unnecessary work, we just ignore the change since we know
// that if the current room ID has changed from what we thought
// it was, it means we're about to be unmounted.
return;
}
const roomId = _RoomViewStore.default.getRoomId();
const newState
/*: Pick<IState, any>*/
= {
roomId,
roomAlias: _RoomViewStore.default.getRoomAlias(),
roomLoading: _RoomViewStore.default.isRoomLoading(),
roomLoadError: _RoomViewStore.default.getRoomLoadError(),
joining: _RoomViewStore.default.isJoining(),
initialEventId: _RoomViewStore.default.getInitialEventId(),
isInitialEventHighlighted: _RoomViewStore.default.isInitialEventHighlighted(),
replyToEvent: _RoomViewStore.default.getQuotingEvent(),
forwardingEvent: _RoomViewStore.default.getForwardingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && _RoomViewStore.default.shouldPeek(),
showingPinned: _SettingsStore.default.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: _SettingsStore.default.getValue("showReadReceipts", roomId),
wasContextSwitch: _RoomViewStore.default.getWasContextSwitch()
};
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.stopPeeking();
} // Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307
console.log('RVS update:', newState.roomId, newState.roomAlias, 'loading?', newState.roomLoading, 'joining?', newState.joining, 'initial?', initial, 'shouldPeek?', newState.shouldPeek); // NB: This does assume that the roomID will not change for the lifetime of
// the RoomView instance
if (initial) {
newState.room = this.context.getRoom(newState.roomId);
if (newState.room) {
newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room);
}
}
if (this.state.roomId === null && newState.roomId !== null) {
// Get the scroll state for the new room
// If an event ID wasn't specified, default to the one saved for this room
// in the scroll state store. Assume initialEventPixelOffset should be set.
if (!newState.initialEventId) {
const roomScrollState = _RoomScrollStateStore.default.getScrollState(newState.roomId);
if (roomScrollState) {
newState.initialEventId = roomScrollState.focussedEvent;
newState.initialEventPixelOffset = roomScrollState.pixelOffset;
}
}
} // Clear the search results when clicking a search result (which changes the
// currently scrolled to event, this.state.initialEventId).
if (this.state.initialEventId !== newState.initialEventId) {
newState.searchResults = null;
}
this.setState(newState); // At this point, newState.roomId could be null (e.g. the alias might not
// have been resolved yet) so anything called here must handle this case.
// We pass the new state into this function for it to read: it needs to
// observe the new state but we don't want to put it in the setState
// callback because this would prevent the setStates from being batched,
// ie. cause it to render RoomView twice rather than the once that is necessary.
if (initial) {
this.setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek);
}
});
(0, _defineProperty2.default)(this, "getRoomId", () => {
// According to `onRoomViewStoreUpdate`, `state.roomId` can be null
// if we have a room alias we haven't resolved yet. To work around this,
// first we'll try the room object if it's there, and then fallback to
// the bare room ID. (We may want to update `state.roomId` after
// resolving aliases, so we could always trust it.)
return this.state.room ? this.state.room.roomId : this.state.roomId;
});
(0, _defineProperty2.default)(this, "onWidgetEchoStoreUpdate", () => {
if (!this.state.room) return;
this.setState({
hasPinnedWidgets: _WidgetLayoutStore.WidgetLayoutStore.instance.getContainerWidgets(this.state.room, _WidgetLayoutStore.Container.Top).length > 0,
showApps: this.shouldShowApps(this.state.room)
});
});
(0, _defineProperty2.default)(this, "onWidgetLayoutChange", () => {
this.onWidgetEchoStoreUpdate(); // we cheat here by calling the thing that matters
});
(0, _defineProperty2.default)(this, "onLayoutChange", () => {
this.setState({
layout: _SettingsStore.default.getValue("layout")
});
});
(0, _defineProperty2.default)(this, "onRightPanelStoreUpdate", () => {
this.setState({
showRightPanel: _RightPanelStore.default.getSharedInstance().isOpenForRoom
});
});
(0, _defineProperty2.default)(this, "onPageUnload", event => {
if (_ContentMessages.default.sharedInstance().getCurrentUploads().length > 0) {
return event.returnValue = (0, _languageHandler._t)("You seem to be uploading files, are you sure you want to quit?");
} else if (this.getCallForRoom() && this.state.callState !== 'ended') {
return event.returnValue = (0, _languageHandler._t)("You seem to be in a call, are you sure you want to quit?");
}
});
(0, _defineProperty2.default)(this, "onReactKeyDown", ev => {
let handled = false;
const action = (0, _KeyBindingsManager.getKeyBindingsManager)().getRoomAction(ev);
switch (action) {
case _KeyBindingsManager.RoomAction.DismissReadMarker:
this.messagePanel.forgetReadMarker();
this.jumpToLiveTimeline();
handled = true;
break;
case _KeyBindingsManager.RoomAction.JumpToOldestUnread:
this.jumpToReadMarker();
handled = true;
break;
case _KeyBindingsManager.RoomAction.UploadFile:
_dispatcher.default.dispatch({
action: "upload_file"
}, true);
handled = true;
break;
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
}
});
(0, _defineProperty2.default)(this, "onAction", payload => {
switch (payload.action) {
case 'message_sent':
this.checkDesktopNotifications();
break;
case 'post_sticker_message':
this.injectSticker(payload.data.content.url, payload.data.content.info, payload.data.description || payload.data.name);
break;
case 'picture_snapshot':
_ContentMessages.default.sharedInstance().sendContentListToRoom([payload.file], this.state.room.roomId, this.context);
break;
case 'notifier_enabled':
case _actions.Action.UploadStarted:
case _actions.Action.UploadFinished:
case _actions.Action.UploadCanceled:
this.forceUpdate();
break;
case 'call_state':
{
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (!payload.room_id) {
return;
}
const call = this.getCallForRoom();
this.setState({
callState: call ? call.state : null
});
break;
}
case 'appsDrawer':
this.setState({
showApps: payload.show
});
break;
case 'reply_to_event':
if (this.state.searchResults && payload.event.getRoomId() === this.state.roomId && !this.unmounted) {
this.onCancelSearchClick();
}
break;
case 'quote':
if (this.state.searchResults) {
const roomId = payload.event.getRoomId();
if (roomId === this.state.roomId) {
this.onCancelSearchClick();
}
setImmediate(() => {
_dispatcher.default.dispatch({
action: 'view_room',
room_id: roomId,
deferred_action: payload
});
});
}
break;
case 'sync_state':
if (!this.state.matrixClientIsReady) {
this.setState({
matrixClientIsReady: this.context && this.context.isInitialSyncComplete()
}, () => {
// send another "initial" RVS update to trigger peeking if needed
this.onRoomViewStoreUpdate(true);
});
}
break;
case 'focus_search':
this.onSearchClick();
break;
}
});
(0, _defineProperty2.default)(this, "onRoomTimeline", (ev
/*: MatrixEvent*/
, room
/*: Room*/
, toStartOfTimeline
/*: boolean*/
, removed, data) => {
if (this.unmounted) return; // ignore events for other rooms
if (!room) return;
if (!this.state.room || room.roomId != this.state.room.roomId) return; // ignore events from filtered timelines
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
if (ev.getType() === "org.matrix.room.preview_urls") {
this.updatePreviewUrlVisibility(room);
}
if (ev.getType() === "m.room.encryption") {
this.updateE2EStatus(room);
} // ignore anything but real-time updates at the end of the room:
// updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return; // no point handling anything while we're waiting for the join to finish:
// we'll only be showing a spinner.
if (this.state.joining) return;
if (ev.getSender() !== this.context.credentials.userId) {
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {// no change
} else if (!(0, _shouldHideEvent.default)(ev)) {
this.setState((state, props) => {
return {
numUnreadMessages: state.numUnreadMessages + 1
};
});
}
}
});
(0, _defineProperty2.default)(this, "onEventDecrypted", ev => {
if (ev.isDecryptionFailure()) return;
this.handleEffects(ev);
});
(0, _defineProperty2.default)(this, "onEvent", ev => {
if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
this.handleEffects(ev);
});
(0, _defineProperty2.default)(this, "handleEffects", ev => {
if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
if (ev.getRoomId() !== this.state.room.roomId) return; // not for us
const notifState = _RoomNotificationStateStore.RoomNotificationStateStore.instance.getRoomState(this.state.room);
if (!notifState.isUnread) return;
_effects.CHAT_EFFECTS.forEach(effect => {
if ((0, _utils.containsEmoji)(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
_dispatcher.default.dispatch({
action: `effects.${effect.command}`
});
}
});
});
(0, _defineProperty2.default)(this, "onRoomName", (room
/*: Room*/
) => {
if (this.state.room && room.roomId == this.state.room.roomId) {
this.forceUpdate();
}
});
(0, _defineProperty2.default)(this, "onKeyBackupStatus", () => {
// Key backup status changes affect whether the in-room recovery
// reminder is displayed.
this.forceUpdate();
});
(0, _defineProperty2.default)(this, "canResetTimeline", () => {
if (!this.messagePanel) {
return true;
}
return this.messagePanel.canResetTimeline();
});
(0, _defineProperty2.default)(this, "onRoomLoaded", (room
/*: Room*/
) => {
// Attach a widget store listener only when we get a room
_WidgetLayoutStore.WidgetLayoutStore.instance.on(_WidgetLayoutStore.WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.onWidgetLayoutChange(); // provoke an update
this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
this.loadMembersIfJoined(room);
this.calculateRecommendedVersion(room);
this.updateE2EStatus(room);
this.updatePermissions(room);
this.checkWidgets(room);
});
(0, _defineProperty2.default)(this, "onRoom", (room
/*: Room*/
) => {
if (!room || room.roomId !== this.state.roomId) {
return;
} // Detach the listener if the room is changing for some reason
if (this.state.room) {
_WidgetLayoutStore.WidgetLayoutStore.instance.off(_WidgetLayoutStore.WidgetLayoutStore.emissionForRoom(this.state.room), this.onWidgetLayoutChange);
}
this.setState({
room: room
}, () => {
this.onRoomLoaded(room);
});
});
(0, _defineProperty2.default)(this, "onDeviceVerificationChanged", (userId
/*: string*/
, device
/*: object*/
) => {
const room = this.state.room;
if (!room.currentState.getMember(userId)) {
return;
}
this.updateE2EStatus(room);
});
(0, _defineProperty2.default)(this, "onUserVerificationChanged", (userId
/*: string*/
, trustStatus
/*: object*/
) => {
const room = this.state.room;
if (!room || !room.currentState.getMember(userId)) {
return;
}
this.updateE2EStatus(room);
});
(0, _defineProperty2.default)(this, "onCrossSigningKeysChanged", () => {
const room = this.state.room;
if (room) {
this.updateE2EStatus(room);
}
});
(0, _defineProperty2.default)(this, "onAccountData", (event
/*: MatrixEvent*/
) => {
const type = event.getType();
if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
this.updatePreviewUrlVisibility(this.state.room);
}
});
(0, _defineProperty2.default)(this, "onRoomAccountData", (event
/*: MatrixEvent*/
, room
/*: Room*/
) => {
if (room.roomId == this.state.roomId) {
const type = event.getType();
if (type === "org.matrix.room.color_scheme") {
const colorScheme = event.getContent(); // XXX: we should validate the event
console.log("Tinter.tint from onRoomAccountData");
_Tinter.default.tint(colorScheme.primary_color, colorScheme.secondary_color);
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
this.updatePreviewUrlVisibility(room);
}
}
});
(0, _defineProperty2.default)(this, "onRoomStateEvents", (ev
/*: MatrixEvent*/
, state) => {
// ignore if we don't have a room yet
if (!this.state.room || this.state.room.roomId !== state.roomId) {
return;
}
this.updatePermissions(this.state.room);
});
(0, _defineProperty2.default)(this, "onRoomStateMember", (ev
/*: MatrixEvent*/
, state, member) => {
// ignore if we don't have a room yet
if (!this.state.room) {
return;
} // ignore members in other rooms
if (member.roomId !== this.state.room.roomId) {
return;
}
this.updateRoomMembers(member);
});
(0, _defineProperty2.default)(this, "onMyMembership", (room
/*: Room*/
, membership
/*: string*/
, oldMembership
/*: string*/
) => {
if (room.roomId === this.state.roomId) {
this.forceUpdate();
this.loadMembersIfJoined(room);
this.updatePermissions(room);
}
});
(0, _defineProperty2.default)(this, "updateRoomMembers", (0, _ratelimitedfunc.default)(() => {
this.updateDMState();
this.updateE2EStatus(this.state.room);
}, 500));
(0, _defineProperty2.default)(this, "onSearchResultsFillRequest", (backwards
/*: boolean*/
) => {
if (!backwards) {
return Promise.resolve(false);
}
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
const searchPromise = (0, _Searching.searchPagination)(this.state.searchResults);
return this.handleSearchResult(searchPromise);
} else {
debuglog("no more search results");
return Promise.resolve(false);
}
});
(0, _defineProperty2.default)(this, "onInviteButtonClick", () => {
// call AddressPickerDialog
_dispatcher.default.dispatch({
action: 'view_invite',
roomId: this.state.room.roomId
});
});
(0, _defineProperty2.default)(this, "onJoinButtonClicked", () => {
// If the user is a ROU, allow them to transition to a PWLU
if (this.context && this.context.isGuest()) {
// Join this room once the user has registered and logged in
// (If we failed to peek, we may not have a valid room object.)
_dispatcher.default.dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'view_room',
room_id: this.getRoomId()
}
});
_dispatcher.default.dispatch({
action: 'require_registration'
});
} else {
Promise.resolve().then(() => {
const signUrl = this.props.threepidInvite?.signUrl;
_dispatcher.default.dispatch({
action: 'join_room',
opts: {
inviteSignUrl: signUrl
},
_type: "unknown" // TODO: instrumentation
});
return Promise.resolve();
});
}
});
(0, _defineProperty2.default)(this, "onMessageListScroll", ev => {
if (this.messagePanel.isAtEndOfLiveTimeline()) {
this.setState({
numUnreadMessages: 0,
atEndOfLiveTimeline: true
});
} else {
this.setState({
atEndOfLiveTimeline: false
});
}
this.updateTopUnreadMessagesBar();
});
(0, _defineProperty2.default)(this, "onDragEnter", ev => {
ev.stopPropagation();
ev.preventDefault(); // We always increment the counter no matter the types, because dragging is
// still happening. If we didn't, the drag counter would get out of sync.
this.setState({
dragCounter: this.state.dragCounter + 1
}); // See:
// https://docs.w3cub.com/dom/datatransfer/types
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
this.setState({
draggingFile: true
});
}
});
(0, _defineProperty2.default)(this, "onDragLeave", ev => {
ev.stopPropagation();
ev.preventDefault();
this.setState({
dragCounter: this.state.dragCounter - 1
});
if (this.state.dragCounter === 0) {
this.setState({
draggingFile: false
});
}
});
(0, _defineProperty2.default)(this, "onDragOver", ev => {
ev.stopPropagation();
ev.preventDefault();
ev.dataTransfer.dropEffect = 'none'; // See:
// https://docs.w3cub.com/dom/datatransfer/types
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
ev.dataTransfer.dropEffect = 'copy';
}
});
(0, _defineProperty2.default)(this, "onDrop", ev => {
ev.stopPropagation();
ev.preventDefault();
_ContentMessages.default.sharedInstance().sendContentListToRoom(ev.dataTransfer.files, this.state.room.roomId, this.context);
_dispatcher.default.fire(_actions.Action.FocusComposer);
this.setState({
draggingFile: false,
dragCounter: this.state.dragCounter - 1
});
});
(0, _defineProperty2.default)(this, "onSearch", (term
/*: string*/
, scope) => {
this.setState({
searchTerm: term,
searchScope: scope,
searchResults: {},
searchHighlights: []
}); // if we already have a search panel, we need to tell it to forget
// about its scroll state.
if (this.searchResultsPanel.current) {
this.searchResultsPanel.current.resetScrollState();
} // make sure that we don't end up showing results from
// an aborted search by keeping a unique id.
//
// todo: should cancel any previous search requests.
this.searchId = new Date().getTime();
let roomId;
if (scope === "Room") roomId = this.state.room.roomId;
debuglog("sending search request");
const searchPromise = (0, _Searching.default)(term, roomId);
this.handleSearchResult(searchPromise);
});
(0, _defineProperty2.default)(this, "onPinnedClick", () => {
const nowShowingPinned = !this.state.showingPinned;
const roomId = this.state.room.roomId;
this.setState({
showingPinned: nowShowingPinned,
searching: false
});
_SettingsStore.default.setValue("PinnedEvents.isOpen", roomId, _SettingLevel.SettingLevel.ROOM_DEVICE, nowShowingPinned);
});
(0, _defineProperty2.default)(this, "onCallPlaced", (type
/*: PlaceCallType*/
) => {
_dispatcher.default.dispatch({
action: 'place_call',
type: type,
room_id: this.state.room.roomId
});
});
(0, _defineProperty2.default)(this, "onSettingsClick", () => {
_dispatcher.default.dispatch({
action: "open_room_settings"
});
});
(0, _defineProperty2.default)(this, "onCancelClick", () => {
console.log("updateTint from onCancelClick");
this.updateTint();
if (this.state.forwardingEvent) {
_dispatcher.default.dispatch({
action: 'forward_event',
event: null
});
}
_dispatcher.default.fire(_actions.Action.FocusComposer);
});
(0, _defineProperty2.default)(this, "onAppsClick", () => {
_dispatcher.default.dispatch({
action: "appsDrawer",
show: !this.state.showApps
});
});
(0, _defineProperty2.default)(this, "onLeaveClick", () => {
_dispatcher.default.dispatch({
action: 'leave_room',
room_id: this.state.room.roomId
});
});
(0, _defineProperty2.default)(this, "onForgetClick", () => {
_dispatcher.default.dispatch({
action: 'forget_room',
room_id: this.state.room.roomId
});
});
(0, _defineProperty2.default)(this, "onRejectButtonClicked", () => {
this.setState({
rejecting: true
});
this.context.leave(this.state.roomId).then(() => {
_dispatcher.default.dispatch({
action: 'view_home_page'
});
this.setState({
rejecting: false
});
}, error => {
console.error("Failed to reject invite: %s", error);
const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
_Modal.default.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: (0, _languageHandler._t)("Failed to reject invite"),
description: msg
});
this.setState({
rejecting: false,
rejectError: error
});
});
});
(0, _defineProperty2.default)(this, "onRejectAndIgnoreClick", async () => {
this.setState({
rejecting: true
});
try {
const myMember = this.state.room.getMember(this.context.getUserId());
const inviteEvent = myMember.events.member;
const ignoredUsers = this.context.getIgnoredUsers();
ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk
await this.context.setIgnoredUsers(ignoredUsers);
await this.context.leave(this.state.roomId);
_dispatcher.default.dispatch({
action: 'view_home_page'
});
this.setState({
rejecting: false
});
} catch (error) {
console.error("Failed to reject invite: %s", error);
const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
_Modal.default.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: (0, _languageHandler._t)("Failed to reject invite"),
description: msg
});
this.setState({
rejecting: false,
rejectError: error
});
}
});
(0, _defineProperty2.default)(this, "onRejectThreepidInviteButtonClicked", () => {
// We can reject 3pid invites in the same way that we accept them,
// using /leave rather than /join. In the short term though, we
// just ignore them.
// https://github.com/vector-im/vector-web/issues/1134
_dispatcher.default.fire(_actions.Action.ViewRoomDirectory);
});
(0, _defineProperty2.default)(this, "onSearchClick", () => {
this.setState({
searching: !this.state.searching,
showingPinned: false
});
});
(0, _defineProperty2.default)(this, "onCancelSearchClick", () => {
this.setState({
searching: false,
searchResults: null
});
});
(0, _defineProperty2.default)(this, "jumpToLiveTimeline", () => {
this.messagePanel.jumpToLiveTimeline();
_dispatcher.default.fire(_actions.Action.FocusComposer);
});
(0, _defineProperty2.default)(this, "jumpToReadMarker", () => {
this.messagePanel.jumpToReadMarker();
});
(0, _defineProperty2.default)(this, "forgetReadMarker", ev => {
ev.stopPropagation();
this.messagePanel.forgetReadMarker();
});
(0, _defineProperty2.default)(this, "updateTopUnreadMessagesBar", () => {
if (!this.messagePanel) {
return;
}
const showBar = this.messagePanel.canJumpToReadMarker();
if (this.state.showTopUnreadMessagesBar != showBar) {
this.setState({
showTopUnreadMessagesBar: showBar
});
}
});
(0, _defineProperty2.default)(this, "onResize", () => {
// It seems flexbox doesn't give us a way to constrain the auxPanel height to have
// a minimum of the height of the video element, whilst also capping it from pushing out the page
// so we have to do it via JS instead. In this implementation we cap the height by putting
// a maxHeight on the underlying remote video tag.
// header + footer + status + give us at least 120px of scrollback at all times.
let auxPanelMaxHeight = window.innerHeight - (54 + // height of RoomHeader
36 + // height of the status area
51 + // minimum height of the message compmoser
120); // amount of desired scrollback
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
// but it's better than the video going missing entirely
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
this.setState({
auxPanelMaxHeight: auxPanelMaxHeight
});
});
(0, _defineProperty2.default)(this, "onFullscreenClick", () => {
_dispatcher.default.dispatch({
action: 'video_fullscreen',
fullscreen: true
}, true);
});
(0, _defineProperty2.default)(this, "onMuteAudioClick", () => {
const call = this.getCallForRoom();
if (!call) {
return;
}
const newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState);
this.forceUpdate(); // TODO: just update the voip buttons
});
(0, _defineProperty2.default)(this, "onMuteVideoClick", () => {
const call = this.getCallForRoom();
if (!call) {
return;
}
const newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState);
this.forceUpdate(); // TODO: just update the voip buttons
});
(0, _defineProperty2.default)(this, "onStatusBarVisible", () => {
if (this.unmounted) return;
this.setState({
statusBarVisible: true
});
});
(0, _defineProperty2.default)(this, "onStatusBarHidden", () => {
// This is currently not desired as it is annoying if it keeps expanding and collapsing
if (this.unmounted) return;
this.setState({
statusBarVisible: false
});
});
(0, _defineProperty2.default)(this, "handleScrollKey", ev => {
let panel;
if (this.searchResultsPanel.current) {
panel = this.searchResultsPanel.current;
} else if (this.messagePanel) {
panel = this.messagePanel;
}
if (panel) {
panel.handleScrollKey(ev);
}
});
(0, _defineProperty2.default)(this, "gatherTimelinePanelRef", r => {
this.messagePanel = r;
if (r) {
console.log("updateTint from RoomView.gatherTimelinePanelRef");
this.updateTint();
}
});
(0, _defineProperty2.default)(this, "onHiddenHighlightsClick", () => {
const oldRoom = this.getOldRoom();
if (!oldRoom) return;
_dispatcher.default.dispatch({
action: "view_room",
room_id: oldRoom.roomId
});
});
const llMembers = this.context.hasLazyLoadMembersEnabled();
this.state = {
roomId: null,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
membersLoaded: !llMembers,
numUnreadMessages: 0,
draggingFile: false,
searching: false,
searchResults: null,
callState: null,
guestsCanJoin: false,
canPeek: false,
showApps: false,
isPeeking: false,
showingPinned: false,
showReadReceipts: true,
showRightPanel: _RightPanelStore.default.getSharedInstance().isOpenForRoom,
joining: false,
atEndOfLiveTimeline: true,
atEndOfLiveTimelineInit: false,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canReply: false,
layout: _SettingsStore.default.getValue("layout"),
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
dragCounter: 0
};
this.dispatcherRef = _dispatcher.default.register(this.onAction);
this.context.on("Room", this.onRoom);
this.context.on("Room.timeline", this.onRoomTimeline);
this.context.on("Room.name", this.onRoomName);
this.context.on("Room.accountData", this.onRoomAccountData);
this.context.on("RoomState.events", this.onRoomStateEvents);
this.context.on("RoomState.members", this.onRoomStateMember);
this.context.on("Room.myMembership", this.onMyMembership);
this.context.on("accountData", this.onAccountData);
this.context.on("crypto.keyBackupStatus", this.onKeyBackupStatus);
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
this.context.on("Event.decrypted", this.onEventDecrypted);
this.context.on("event", this.onEvent); // Start listening for RoomViewStore updates
this.roomStoreToken = _RoomViewStore.default.addListener(this.onRoomViewStoreUpdate);
this.rightPanelStoreToken = _RightPanelStore.default.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
_WidgetEchoStore.default.on(_AsyncStore.UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
_WidgetStore.default.instance.on(_AsyncStore.UPDATE_EVENT, this.onWidgetStoreUpdate);
this.showReadReceiptsWatchRef = _SettingsStore.default.watchSetting("showReadReceipts", null, this.onReadReceiptsChange);
this.layoutWatcherRef = _SettingsStore.default.watchSetting("layout", null, this.onLayoutChange);
}
getPermalinkCreatorForRoom(room
/*: Room*/
) {
if (this.permalinkCreators[room.roomId]) return this.permalinkCreators[room.roomId];
this.permalinkCreators[room.roomId] = new _Permalinks.RoomPermalinkCreator(room);
if (this.state.room && room.roomId === this.state.room.roomId) {
// We want to watch for changes in the creator for the primary room in the view, but
// don't need to do so for search results.
this.permalinkCreators[room.roomId].start();
} else {
this.permalinkCreators[room.roomId].load();
}
return this.permalinkCreators[room.roomId];
}
stopAllPermalinkCreators() {
if (!this.permalinkCreators) return;
for (const roomId of Object.keys(this.permalinkCreators)) {
this.permalinkCreators[roomId].stop();
}
}
setupRoom(room
/*: Room*/
, roomId
/*: string*/
, joining
/*: boolean*/
, shouldPeek
/*: boolean*/
) {
// if this is an unknown room then we're in one of three states:
// - This is a room we can peek into (search engine) (we can /peek)
// - This is a room we can publicly join or were invited to. (we can /join)
// - This is a room we cannot join at all. (no action can help us)
// We can't try to /join because this may implicitly accept invites (!)
// We can /peek though. If it fails then we present the join UI. If it
// succeeds then great, show the preview (but we still may be able to /join!).
// Note that peeking works by room ID and room ID only, as opposed to joining
// which must be by alias or invite wherever possible (peeking currently does
// not work over federation).
// NB. We peek if we have never seen the room before (i.e. js-sdk does not know
// about it). We don't peek in the historical case where we were joined but are
// now not joined because the js-sdk peeking API will clobber our historical room,
// making it impossible to indicate a newly joined room.
if (!joining && roomId) {
if (!room && shouldPeek) {
console.info("Attempting to peek into room %s", roomId);
this.setState({
peekLoading: true,
isPeeking: true // this will change to false if peeking fails
});
this.context.peekInRoom(roomId).then(room => {
if (this.unmounted) {
return;
}
this.setState({
room: room,
peekLoading: false
});
this.onRoomLoaded(room);
}).catch(err => {
if (this.unmounted) {
return;
} // Stop peeking if anything went wrong
this.setState({
isPeeking: false
}); // This won't necessarily be a MatrixError, but we duck-type
// here and say if it's got an 'errcode' key with the right value,
// it means we can't peek.
if (err.errcode === "M_GUEST_ACCESS_FORBIDDEN" || err.errcode === 'M_FORBIDDEN') {
// This is fine: the room just isn't peekable (we assume).
this.setState({
peekLoading: false
});
} else {
throw err;
}
});
} else if (room) {
// Stop peeking because we have joined this room previously
this.context.stopPeeking();
this.setState({
isPeeking: false
});
}
}
}
shouldShowApps(room
/*: Room*/
) {
if (!BROWSER_SUPPORTS_SANDBOX || !room) return false; // Check if user has previously chosen to hide the app drawer for this
// room. If so, do not show apps
const hideWidgetDrawer = localStorage.getItem(room.roomId + "_hide_widget_drawer"); // This is confusing, but it means to say that we default to the tray being
// hidden unless the user clicked to open it.
const isManuallyShown = hideWidgetDrawer === "false";
const widgets = _WidgetLayoutStore.WidgetLayoutStore.instance.getContainerWidgets(room, _WidgetLayoutStore.Container.Top);
return widgets.length > 0 || isManuallyShown;
}
componentDidMount() {
this.onRoomViewStoreUpdate(true);
const call = this.getCallForRoom();
const callState = call ? call.state : null;
this.setState({
callState: callState
});
window.addEventListener('beforeunload', this.onPageUnload);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
}
this.onResize();
}
shouldComponentUpdate(nextProps, nextState) {
return (0, _objects.objectHasDiff)(this.props, nextProps) || (0, _objects.objectHasDiff)(this.state, nextState);
}
componentDidUpdate() {
if (this.roomView.current) {
const roomView = this.roomView.current;
if (!roomView.ondrop) {
roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver);
roomView.addEventListener('dragenter', this.onDragEnter);
roomView.addEventListener('dragleave', this.onDragLeave);
}
} // Note: We check the ref here with a flag because componentDidMount, despite
// documentation, does not define our messagePanel ref. It looks like our spinner
// in render() prevents the ref from being set on first mount, so we try and
// catch the messagePanel when it does mount. Because we only want the ref once,
// we use a boolean flag to avoid duplicate work.
if (this.messagePanel && !this.state.atEndOfLiveTimelineInit) {
this.setState({
atEndOfLiveTimelineInit: true,
atEndOfLiveTimeline: this.messagePanel.isAtEndOfLiveTimeline()
});
}
}
componentWillUnmount() {
// set a boolean to say we've been unmounted, which any pending
// promises can use to throw away their results.
//
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true; // update the scroll map before we get unmounted
if (this.state.roomId) {
_RoomScrollStateStore.default.setScrollState(this.state.roomId, this.getScrollState());
}
if (this.state.shouldPeek) {
this.context.stopPeeking();
} // stop tracking room changes to format permalinks
this.stopAllPermalinkCreators();
if (this.roomView.current) {
// disconnect the D&D event listeners from the room view. This
// is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up.
const roomView = this.roomView.current;
roomView.removeEventListener('drop', this.onDrop);
roomView.removeEventListener('dragover', this.onDragOver);
roomView.removeEventListener('dragenter', this.onDragEnter);
roomView.removeEventListener('dragleave', this.onDragLeave);
}
_dispatcher.default.unregister(this.dispatcherRef);
if (this.context) {
this.context.removeListener("Room", this.onRoom);
this.context.removeListener("Room.timeline", this.onRoomTimeline);
this.context.removeListener("Room.name", this.onRoomName);
this.context.removeListener("Room.accountData", this.onRoomAccountData);
this.context.removeListener("RoomState.events", this.onRoomStateEvents);
this.context.removeListener("Room.myMembership", this.onMyMembership);
this.context.removeListener("RoomState.members", this.onRoomStateMember);
this.context.removeListener("accountData", this.onAccountData);
this.context.removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
this.context.removeListener("Event.d