UNPKG

matrix-react-sdk

Version:
1,370 lines (1,165 loc) 273 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _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