diff --git a/res/css/_components.scss b/res/css/_components.scss index 0b46df9bd8..4884b0a036 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -115,6 +115,7 @@ @import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_ManageIntegsButton.scss"; +@import "./views/elements/_MiniAvatarUploader.scss"; @import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_ProgressBar.scss"; @import "./views/elements/_QRCode.scss"; @@ -139,6 +140,7 @@ @import "./views/groups/_GroupUserSettings.scss"; @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; +@import "./views/messages/_EventTileBubble.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; @@ -182,6 +184,7 @@ @import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MessageComposer.scss"; @import "./views/rooms/_MessageComposerFormatBar.scss"; +@import "./views/rooms/_NewRoomIntro.scss"; @import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PinnedEventsPanel.scss"; diff --git a/res/css/structures/_HomePage.scss b/res/css/structures/_HomePage.scss index 2077582a7d..45aa34d3b5 100644 --- a/res/css/structures/_HomePage.scss +++ b/res/css/structures/_HomePage.scss @@ -50,42 +50,8 @@ limitations under the License. color: $muted-fg-color; } - .mx_HomePage_userAvatar { - position: relative; - width: min-content; + .mx_MiniAvatarUploader { margin: 0 auto; - - &::before, &::after { - content: ''; - position: absolute; - - height: 26px; - width: 26px; - - right: -6px; - bottom: -6px; - } - - &::before { - background-color: $primary-bg-color; - border-radius: 50%; - z-index: 1; - } - - &::after { - background-color: $secondary-fg-color; - mask-position: center; - mask-repeat: no-repeat; - mask-image: url('$(res)/img/element-icons/camera.svg'); - mask-size: 16px; - z-index: 2; - } - - &.mx_HomePage_userAvatar_busy::after { - background: url("$(res)/img/spinner.gif") no-repeat center; - background-size: 80%; - mask: unset; - } } .mx_HomePage_default_buttons { diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index cd4390ee5c..2d5359c0eb 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -153,16 +153,6 @@ limitations under the License. display: block; } -.mx_RoomStatusBar_isAlone { - height: 50px; - line-height: $font-50px; - - color: $primary-fg-color; - opacity: 0.5; - overflow-y: hidden; - display: block; -} - .mx_MatrixChat_useCompactLayout { .mx_RoomStatusBar { min-height: 40px; diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss new file mode 100644 index 0000000000..2502977331 --- /dev/null +++ b/res/css/views/elements/_MiniAvatarUploader.scss @@ -0,0 +1,56 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_MiniAvatarUploader { + position: relative; + width: min-content; + + &::before, &::after { + content: ''; + position: absolute; + + height: 26px; + width: 26px; + + right: -6px; + bottom: -6px; + } + + &::before { + background-color: $primary-bg-color; + border-radius: 50%; + z-index: 1; + } + + &::after { + background-color: $secondary-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/camera.svg'); + mask-size: 16px; + z-index: 2; + } + + &.mx_MiniAvatarUploader_busy::after { + background: url("$(res)/img/spinner.gif") no-repeat center; + background-size: 80%; + mask: unset; + } +} + +.mx_MiniAvatarUploader_input { + display: none; +} diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index d45645863f..cb2bf841dd 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,25 +15,8 @@ limitations under the License. */ .mx_CreateEvent { - background-color: $info-plinth-bg-color; - padding-left: 20px; - padding-right: 20px; - padding-top: 10px; - padding-bottom: 10px; -} - -.mx_CreateEvent_image { - float: left; - margin-right: 20px; - width: 72px; - height: 34px; - - background-color: $primary-fg-color; - mask: url('$(res)/img/room-continuation.svg'); - mask-repeat: no-repeat; - mask-position: center; -} - -.mx_CreateEvent_header { - font-weight: bold; + &::before { + background-color: $composer-e2e-icon-color; + mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); + } } diff --git a/res/css/views/messages/_EventTileBubble.scss b/res/css/views/messages/_EventTileBubble.scss new file mode 100644 index 0000000000..e0f5d521cb --- /dev/null +++ b/res/css/views/messages/_EventTileBubble.scss @@ -0,0 +1,60 @@ +/* +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_EventTileBubble { + background-color: $dark-panel-bg-color; + padding: 10px; + border-radius: 8px; + margin: 10px auto; + max-width: 75%; + box-sizing: border-box; + display: grid; + grid-template-columns: 24px minmax(0, 1fr) min-content; + + &::before, &::after { + position: relative; + grid-column: 1; + grid-row: 1 / 3; + width: 16px; + height: 16px; + content: ""; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + margin-top: 4px; + } + + .mx_EventTileBubble_title, .mx_EventTileBubble_subtitle { + overflow-wrap: break-word; + } + + .mx_EventTileBubble_title { + font-weight: 600; + font-size: $font-15px; + grid-column: 2; + grid-row: 1; + } + + .mx_EventTileBubble_subtitle { + font-size: $font-12px; + grid-column: 2; + grid-row: 2; + } +} diff --git a/res/css/views/messages/_MJitsiWidgetEvent.scss b/res/css/views/messages/_MJitsiWidgetEvent.scss index 3e51e89744..bea8651543 100644 --- a/res/css/views/messages/_MJitsiWidgetEvent.scss +++ b/res/css/views/messages/_MJitsiWidgetEvent.scss @@ -15,41 +15,8 @@ limitations under the License. */ .mx_MJitsiWidgetEvent { - display: grid; - grid-template-columns: 24px minmax(0, 1fr) min-content; - &::before { - grid-column: 1; - grid-row: 1 / 3; - width: 16px; - height: 16px; - content: ""; - top: 0; - bottom: 0; - left: 0; - right: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; background-color: $composer-e2e-icon-color; // XXX: Variable abuse - margin-top: 4px; mask-image: url('$(res)/img/element-icons/call/video-call.svg'); } - - .mx_MJitsiWidgetEvent_title { - font-weight: 600; - font-size: $font-15px; - grid-column: 2; - grid-row: 1; - } - - .mx_MJitsiWidgetEvent_subtitle { - grid-column: 2; - grid-row: 2; - } - - .mx_MJitsiWidgetEvent_title, - .mx_MJitsiWidgetEvent_subtitle { - overflow-wrap: break-word; - } } diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index 09c78ae5b4..4faa4b594f 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -15,28 +15,6 @@ limitations under the License. */ .mx_cryptoEvent { - display: grid; - grid-template-columns: 24px minmax(0, 1fr) min-content; - - &.mx_cryptoEvent_icon::before, - &.mx_cryptoEvent_icon::after { - grid-column: 1; - grid-row: 1 / 3; - width: 16px; - height: 16px; - content: ""; - top: 0; - bottom: 0; - left: 0; - right: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url('$(res)/img/e2e/normal.svg'); - background-color: $composer-e2e-icon-color; - margin-top: 4px; - } - // white infill for the transparency &.mx_cryptoEvent_icon::before { background-color: #ffffff; @@ -46,6 +24,11 @@ limitations under the License. mask-size: 90%; } + &.mx_cryptoEvent_icon::after { + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; + } + &.mx_cryptoEvent_icon_verified::after { mask-image: url("$(res)/img/e2e/verified.svg"); background-color: $accent-color; @@ -56,25 +39,6 @@ limitations under the License. background-color: $notice-primary-color; } - .mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state { - overflow-wrap: break-word; - } - - .mx_cryptoEvent_title { - font-weight: 600; - font-size: $font-15px; - grid-column: 2; - grid-row: 1; - } - - .mx_cryptoEvent_subtitle { - grid-column: 2; - grid-row: 2; - } - - .mx_cryptoEvent_state, .mx_cryptoEvent_subtitle { - font-size: $font-12px; - } .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; @@ -92,5 +56,7 @@ limitations under the License. margin: auto 0; text-align: center; color: $notice-secondary-color; + overflow-wrap: break-word; + font-size: $font-12px; } } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3b9a491db5..429ac7ed4b 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -25,15 +25,6 @@ $left-gutter: 64px; position: relative; } -.mx_EventTile_bubble { - background-color: $dark-panel-bg-color; - padding: 10px; - border-radius: 5px; - margin: 10px auto; - max-width: 75%; - box-sizing: border-box; -} - .mx_EventTile.mx_EventTile_info { padding-top: 0px; } @@ -131,9 +122,10 @@ $left-gutter: 64px; grid-template-columns: 1fr 100px; .mx_EventTile_line { - margin-right: 0px; + margin-right: 0; grid-column: 1 / 3; - padding: 0; + // override default padding of mx_EventTile_line so that we can be centered + padding: 0 !important; } .mx_EventTile_msgOption { diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss new file mode 100644 index 0000000000..af72a0dd69 --- /dev/null +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -0,0 +1,67 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_NewRoomIntro { + margin: 80px 0 48px 64px; + + .mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) { + &::before, &::after { + content: unset; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } + + .mx_NewRoomIntro_buttons { + margin-top: 28px; + + .mx_AccessibleButton { + line-height: $font-24px; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 20px; + width: 20px; + height: 20px; + margin-right: 5px; + vertical-align: text-bottom; + } + } + + .mx_NewRoomIntro_inviteButton::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + > h2 { + margin-top: 24px; + font-size: $font-24px; + font-weight: 600; + } + + > p { + margin: 0; + font-size: $font-15px; + color: $secondary-fg-color; + } +} diff --git a/res/img/element-icons/chat-bubbles.svg b/res/img/element-icons/chat-bubbles.svg new file mode 100644 index 0000000000..ac9db61f29 --- /dev/null +++ b/res/img/element-icons/chat-bubbles.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/room-continuation.svg b/res/img/room-continuation.svg deleted file mode 100644 index dc7e15462a..0000000000 --- a/res/img/room-continuation.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Skinner.js b/src/Skinner.js index 87c5a7be7f..d17bc1782a 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -50,8 +50,8 @@ class Skinner { return null; } - // components have to be functions. - const validType = typeof comp === 'function'; + // components have to be functions or forwardRef objects with a render function. + const validType = typeof comp === 'function' || comp.render; if (!validType) { throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`); } diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 8058ddad93..d11944e470 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import * as React from "react"; -import {useContext, useRef, useState} from "react"; +import {useContext, useState} from "react"; import AutoHideScrollbar from './AutoHideScrollbar'; import {getHomePageUrl} from "../../utils/pages"; @@ -24,16 +24,13 @@ import SdkConfig from "../../SdkConfig"; import * as sdk from "../../index"; import dis from "../../dispatcher/dispatcher"; import {Action} from "../../dispatcher/actions"; -import {Transition} from "react-transition-group"; import BaseAvatar from "../views/avatars/BaseAvatar"; import {OwnProfileStore} from "../../stores/OwnProfileStore"; import AccessibleButton from "../views/elements/AccessibleButton"; -import Tooltip from "../views/elements/Tooltip"; import {UPDATE_EVENT} from "../../stores/AsyncStore"; import {useEventEmitter} from "../../hooks/useEventEmitter"; import MatrixClientContext from "../../contexts/MatrixClientContext"; -import classNames from "classnames"; -import {ENTERING} from "react-transition-group/Transition"; +import MiniAvatarUploader, {AVATAR_SIZE} from "../views/elements/MiniAvatarUploader"; const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); const onClickExplore = () => dis.fire(Action.ViewRoomDirectory); @@ -43,11 +40,9 @@ interface IProps { justRegistered?: boolean; } -const avatarSize = 52; - const getOwnProfile = (userId: string) => ({ displayName: OwnProfileStore.instance.displayName || userId, - avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(avatarSize), + avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE), }); const UserWelcomeTop = () => { @@ -57,56 +52,23 @@ const UserWelcomeTop = () => { useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => { setOwnProfile(getOwnProfile(userId)); }); - const [busy, setBusy] = useState(false); - - const uploadRef = useRef(); return
- { - if (!ev.target.files?.length) return; - setBusy(true); - const file = ev.target.files[0]; - const uri = await cli.uploadContent(file); - await cli.setAvatarUrl(uri); - setBusy(false); - }} - accept="image/*" - /> - - { - uploadRef.current.click(); - }} + cli.setAvatarUrl(url)} > - - - {state => ( - - )} - - +

{ _t("Welcome %(name)s", { name: ownProfile.displayName }) }

{ _t("Now, let's help you get started") }

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index e2e3592536..375545f819 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -30,6 +30,8 @@ import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; import {textForEvent} from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; +import DMRoomMap from "../../utils/DMRoomMap"; +import NewRoomIntro from "../views/rooms/NewRoomIntro"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -952,15 +954,25 @@ class CreationGrouper { }).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.shared().getUserIdForRoomId(roomId)) { + summaryText = _t("%(creator)s created this DM.", { creator }); + } else { + summaryText = _t("%(creator)s created and configured the room.", { creator }); + } + + ret.push(); + ret.push( { eventTiles } , diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index e390be6979..e6d2985073 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -41,9 +41,6 @@ export default class RoomStatusBar extends React.Component { static propTypes = { // the room this statusbar is representing. room: PropTypes.object.isRequired, - // This is true when the user is alone in the room, but has also sent a message. - // Used to suggest to the user to invite someone - sentMessageAndIsAlone: PropTypes.bool, // The active call in the room, if any (means we show the call bar // along with the status of the call) @@ -68,10 +65,6 @@ export default class RoomStatusBar extends React.Component { // 'you are alone' bar onInviteClick: PropTypes.func, - // callback for when the user clicks on the 'stop warning me' button in the - // 'you are alone' bar - onStopWarningClick: PropTypes.func, - // callback for when we do something that changes the size of the // status bar. This is used to trigger a re-layout in the parent // component. @@ -159,10 +152,7 @@ export default class RoomStatusBar extends React.Component { // changed - so we use '0' to indicate normal size, and other values to // indicate other sizes. _getSize() { - if (this._shouldShowConnectionError() || - this._showCallBar() || - this.props.sentMessageAndIsAlone - ) { + if (this._shouldShowConnectionError() || this._showCallBar()) { return STATUS_BAR_EXPANDED; } else if (this.state.unsentMessages.length > 0) { return STATUS_BAR_EXPANDED_LARGE; @@ -325,24 +315,6 @@ export default class RoomStatusBar extends React.Component { ); } - // If you're alone in the room, and have sent a message, suggest to invite someone - if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) { - return ( -
- { _t("There's no one else here! Would you like to invite others " + - "or stop warning about the empty room?", - {}, - { - 'inviteText': (sub) => - { sub }, - 'nowarnText': (sub) => - { sub }, - }, - ) } -
- ); - } - return null; } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1c2bf3a000..229416d40a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -150,7 +150,6 @@ export interface IState { guestsCanJoin: boolean; canPeek: boolean; showApps: boolean; - isAlone: boolean; isPeeking: boolean; showingPinned: boolean; showReadReceipts: boolean; @@ -223,7 +222,6 @@ export default class RoomView extends React.Component { guestsCanJoin: false, canPeek: false, showApps: false, - isAlone: false, isPeeking: false, showingPinned: false, showReadReceipts: true, @@ -705,9 +703,8 @@ export default class RoomView extends React.Component { private onAction = payload => { switch (payload.action) { - case 'message_send_failed': case 'message_sent': - this.checkIfAlone(this.state.room); + this.checkDesktopNotifications(); break; case 'post_sticker_message': this.injectSticker( @@ -1025,36 +1022,15 @@ export default class RoomView extends React.Component { } // rate limited because a power level change will emit an event for every member in the room. - private updateRoomMembers = rateLimitedFunc((dueToMember) => { + private updateRoomMembers = rateLimitedFunc(() => { this.updateDMState(); - - let memberCountInfluence = 0; - if (dueToMember && dueToMember.membership === "invite" && this.state.room.getInvitedMemberCount() === 0) { - // A member got invited, but the room hasn't detected that change yet. Influence the member - // count by 1 to counteract this. - memberCountInfluence = 1; - } - this.checkIfAlone(this.state.room, memberCountInfluence); - this.updateE2EStatus(this.state.room); }, 500); - private checkIfAlone(room: Room, countInfluence?: number) { - let warnedAboutLonelyRoom = false; - if (localStorage) { - warnedAboutLonelyRoom = Boolean(localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId)); - } - if (warnedAboutLonelyRoom) { - if (this.state.isAlone) this.setState({isAlone: false}); - return; - } - - let joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount(); - if (countInfluence) joinedOrInvitedMemberCount += countInfluence; - this.setState({isAlone: joinedOrInvitedMemberCount === 1}); - - // if they are not alone additionally prompt the user about notifications so they don't miss replies - if (joinedOrInvitedMemberCount > 1 && Notifier.shouldShowPrompt()) { + private checkDesktopNotifications() { + const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount(); + // if they are not alone prompt the user about notifications so they don't miss replies + if (memberCount > 1 && Notifier.shouldShowPrompt()) { showNotificationsToast(true); } } @@ -1091,14 +1067,6 @@ export default class RoomView extends React.Component { action: 'view_invite', roomId: this.state.room.roomId, }); - this.setState({isAlone: false}); // there's a good chance they'll invite someone - }; - - private onStopAloneWarningClick = () => { - if (localStorage) { - localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, String(true)); - } - this.setState({isAlone: false}); }; private onJoinButtonClicked = () => { @@ -1797,12 +1765,10 @@ export default class RoomView extends React.Component { isStatusAreaExpanded = this.state.statusBarVisible; statusBar = ; diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index cbdae765f7..98d69a63e7 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -35,6 +35,7 @@ interface IProps { height?: number; resizeMethod?: ResizeMethod; viewAvatarOnClick?: boolean; + onClick?(): void; } interface IState { @@ -130,7 +131,7 @@ export default class RoomAvatar extends React.Component { }; public render() { - const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props; + const {room, oobData, viewAvatarOnClick, onClick, ...otherProps} = this.props; const roomName = room ? room.name : oobData.name; @@ -139,7 +140,7 @@ export default class RoomAvatar extends React.Component { name={roomName} idName={room ? room.roomId : null} urls={this.state.urls} - onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : null} + onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick} /> ); } diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx new file mode 100644 index 0000000000..b5e117b42a --- /dev/null +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useContext, useRef, useState} from 'react'; +import classNames from 'classnames'; + +import AccessibleButton from "./AccessibleButton"; +import Tooltip from './Tooltip'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {useTimeout} from "../../../hooks/useTimeout"; + +export const AVATAR_SIZE = 52; + +interface IProps { + hasAvatar: boolean; + noAvatarLabel?: string; + hasAvatarLabel?: string; + setAvatarUrl(url: string): Promise; +} + +const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => { + const cli = useContext(MatrixClientContext); + const [busy, setBusy] = useState(false); + const [hover, setHover] = useState(false); + const [show, setShow] = useState(false); + + useTimeout(() => { + setShow(true); + }, 3000); // show after 3 seconds + useTimeout(() => { + setShow(false); + }, 13000); // hide after being shown for 10 seconds + + const uploadRef = useRef(); + + const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel; + + return + { + if (!ev.target.files?.length) return; + setBusy(true); + const file = ev.target.files[0]; + const uri = await cli.uploadContent(file); + await setAvatarUrl(uri); + setBusy(false); + }} + accept="image/*" + /> + + { + uploadRef.current.click(); + }} + onMouseOver={() => setHover(true)} + onMouseLeave={() => setHover(false)} + > + { children } + + + + ; +}; + +export default MiniAvatarUploader; diff --git a/src/components/views/messages/EncryptionEvent.js b/src/components/views/messages/EncryptionEvent.js deleted file mode 100644 index a9ce10d202..0000000000 --- a/src/components/views/messages/EncryptionEvent.js +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; - -export default class EncryptionEvent extends React.Component { - render() { - const {mxEvent} = this.props; - - let body; - let classes = "mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon"; - const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId()); - if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { - body =
-
{_t("Encryption enabled")}
-
- {_t( - "Messages in this room are end-to-end encrypted. " + - "Learn more & verify this user in their user profile.", - )} -
-
; - } else if (isRoomEncrypted) { - body =
-
{_t("Encryption enabled")}
-
- {_t("Ignored attempt to disable encryption")} -
-
; - } else { - body =
-
{_t("Encryption not enabled")}
-
{_t("The encryption used by this room isn't supported.")}
-
; - classes += " mx_cryptoEvent_icon_warning"; - } - - return (
- {body} -
); - } -} - -EncryptionEvent.propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, -}; diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx new file mode 100644 index 0000000000..3af9c463c9 --- /dev/null +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -0,0 +1,68 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {forwardRef, useContext} from 'react'; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; + +import { _t } from '../../../languageHandler'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import EventTileBubble from "./EventTileBubble"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import DMRoomMap from "../../../utils/DMRoomMap"; + +interface IProps { + mxEvent: MatrixEvent; +} + +const EncryptionEvent = forwardRef(({mxEvent}, ref) => { + const cli = useContext(MatrixClientContext); + const roomId = mxEvent.getRoomId(); + const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); + + if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { + let subtitle: string; + const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); + if (dmPartner) { + const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner; + subtitle = _t("Messages here are end-to-end encrypted. " + + "Verify %(displayName)s in their profile - tap on their avatar.", { displayName }); + } else { + subtitle = _t("Messages in this room are end-to-end encrypted. " + + "When people join, you can verify them in their profile, just tap on their avatar."); + } + + return ; + } else if (isRoomEncrypted) { + return ; + } + + return ; +}); + +export default EncryptionEvent; diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx new file mode 100644 index 0000000000..f797a97a3d --- /dev/null +++ b/src/components/views/messages/EventTileBubble.tsx @@ -0,0 +1,34 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {forwardRef, ReactNode} from "react"; +import classNames from "classnames"; + +interface IProps { + className: string; + title: string; + subtitle?: ReactNode; +} + +const EventTileBubble = forwardRef(({ className, title, subtitle, children }, ref) => { + return
+
{ title }
+ { subtitle &&
{ subtitle }
} + { children } +
; +}); + +export default EventTileBubble; diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 3d191209f9..82aa32d3b7 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import WidgetStore from "../../../stores/WidgetStore"; +import EventTileBubble from "./EventTileBubble"; interface IProps { mxEvent: MatrixEvent; @@ -40,37 +41,24 @@ export default class MJitsiWidgetEvent extends React.PureComponent { if (!url) { // removed - return ( -
-
- {_t('Video conference ended by %(senderName)s', {senderName})} -
-
- ); + return ; } else if (prevUrl) { // modified - return ( -
-
- {_t('Video conference updated by %(senderName)s', {senderName})} -
-
- {joinCopy} -
-
- ); + return ; } else { // assume added - return ( -
-
- {_t("Video conference started by %(senderName)s", {senderName})} -
-
- {joinCopy} -
-
- ); + return ; } } } diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js index ececfc60ed..880299d29d 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.js +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -21,6 +21,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import {getNameForEventRoom, userLabelForEventRoom} from '../../../utils/KeyVerificationStateObserver'; +import EventTileBubble from "./EventTileBubble"; export default class MKeyVerificationConclusion extends React.Component { constructor(props) { @@ -115,14 +116,14 @@ export default class MKeyVerificationConclusion extends React.Component { } if (title) { - const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId()); - const classes = classNames("mx_EventTile_bubble", "mx_cryptoEvent", "mx_cryptoEvent_icon", { + const classes = classNames("mx_cryptoEvent mx_cryptoEvent_icon", { mx_cryptoEvent_icon_verified: request.done, }); - return (
-
{title}
-
{subtitle}
-
); + return ; } return null; diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js index 01a5c2663e..d9594091c5 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -24,6 +24,7 @@ import {getNameForEventRoom, userLabelForEventRoom} import dis from "../../../dispatcher/dispatcher"; import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import {Action} from "../../../dispatcher/actions"; +import EventTileBubble from "./EventTileBubble"; export default class MKeyVerificationRequest extends React.Component { constructor(props) { @@ -146,10 +147,8 @@ export default class MKeyVerificationRequest extends React.Component { if (!request.initiatedByMe) { const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId()); - title = (
{ - _t("%(name)s wants to verify", {name})}
); - subtitle = (
{ - userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}
); + title = _t("%(name)s wants to verify", {name}); + subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId()); if (request.canAccept) { stateNode = (
@@ -157,18 +156,18 @@ export default class MKeyVerificationRequest extends React.Component {
); } } else { // request sent by us - title = (
{ - _t("You sent a verification request")}
); - subtitle = (
{ - userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}
); + title = _t("You sent a verification request"); + subtitle = userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId()); } if (title) { - return (
- {title} - {subtitle} - {stateNode} -
); + return + { stateNode } + ; } return null; } diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js index 6098b1217e..479592aa42 100644 --- a/src/components/views/messages/RoomCreate.js +++ b/src/components/views/messages/RoomCreate.js @@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import EventTileBubble from "./EventTileBubble"; export default class RoomCreate extends React.Component { static propTypes = { @@ -51,17 +52,16 @@ export default class RoomCreate extends React.Component { const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']); permalinkCreator.load(); const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']); - return
-
-
- {_t("This room is a continuation of another conversation.")} -
- + const link = ( + {_t("Click here to see older messages.")} -
; + ); + + return ; } } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 48ab6831d9..c358ef610d 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -21,6 +21,7 @@ import ReplyThread from "../elements/ReplyThread"; import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import classNames from "classnames"; +import {EventType} from "matrix-js-sdk/src/@types/event"; import { _t, _td } from '../../../languageHandler'; import * as TextForEvent from "../../../TextForEvent"; import * as sdk from "../../../index"; @@ -646,12 +647,13 @@ export default class EventTile extends React.Component { // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || - (eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) || - (eventType === "m.room.encryption") || + (eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) || + (eventType === EventType.RoomCreate) || + (eventType === EventType.RoomEncryption) || (tileHandler === "messages.MJitsiWidgetEvent"); let isInfoMessage = ( - !isBubbleMessage && eventType !== 'm.room.message' && - eventType !== 'm.sticker' && eventType !== 'm.room.create' + !isBubbleMessage && eventType !== EventType.RoomMessage && + eventType !== EventType.Sticker && eventType !== EventType.RoomCreate ); // If we're showing hidden events in the timeline, we should use the diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx new file mode 100644 index 0000000000..27404eef12 --- /dev/null +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -0,0 +1,131 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useContext} from "react"; +import {EventType} from "matrix-js-sdk/src/@types/event"; + +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import RoomContext from "../../../contexts/RoomContext"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {_t} from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton"; +import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader"; +import RoomAvatar from "../avatars/RoomAvatar"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; +import {Action} from "../../../dispatcher/actions"; +import dis from "../../../dispatcher/dispatcher"; + +const NewRoomIntro = () => { + const cli = useContext(MatrixClientContext); + const {room, roomId} = useContext(RoomContext); + + const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); + let body; + if (dmPartner) { + let caption; + if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) { + caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join."); + } + + const member = room?.getMember(dmPartner); + const displayName = member?.rawDisplayName || dmPartner; + body = + { + defaultDispatcher.dispatch({ + action: Action.ViewUser, + // XXX: We should be using a real member object and not assuming what the receiver wants. + member: member || {userId: dmPartner}, + }); + }} /> + +

{ room.name }

+ +

{_t("This is the beginning of your direct message history with .", {}, { + displayName: () => { displayName }, + })}

+ { caption &&

{ caption }

} +
; + } else { + const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; + const canAddTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId()); + + const onTopicClick = () => { + dis.dispatch({ + action: "open_room_settings", + room_id: roomId, + }, true); + // focus the topic field to help the user find it as it'll gain an outline + setImmediate(() => { + window.document.getElementById("profileTopic").focus(); + }); + }; + + let topicText; + if (canAddTopic && topic) { + topicText = _t("Topic: %(topic)s (edit)", { topic }, { + a: sub => { sub }, + }); + } else if (topic) { + topicText = _t("Topic: %(topic)s ", { topic }); + } else if (canAddTopic) { + topicText = _t("Add a topic to help people know what it is about.", {}, { + a: sub => { sub }, + }); + } + + const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender(); + const creatorName = room?.getMember(creator)?.rawDisplayName || creator; + + let createdText; + if (creator === cli.getUserId()) { + createdText = _t("You created this room."); + } else { + createdText = _t("%(displayName)s created this room.", { + displayName: creatorName, + }); + } + + const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; + body = + cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')} + > + + + +

{ room.name }

+ +

{createdText} {_t("This is the start of .", {}, { + roomName: () => { room.name }, + })}

+

{topicText}

+
+ + {_t("Invite to this room")} + +
+
; + } + + return
+ { body } +
; +}; + +export default NewRoomIntro; diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index e8eb0c23b4..082dcc4e6b 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -29,7 +29,6 @@ const RoomContext = createContext({ guestsCanJoin: false, canPeek: false, showApps: false, - isAlone: false, isPeeking: false, showingPinned: false, showReadReceipts: true, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5fec27c7f6..830d3cdee4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1335,6 +1335,15 @@ "Strikethrough": "Strikethrough", "Code block": "Code block", "Quote": "Quote", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.", + "This is the beginning of your direct message history with .": "This is the beginning of your direct message history with .", + "Topic: %(topic)s (edit)": "Topic: %(topic)s (edit)", + "Topic: %(topic)s ": "Topic: %(topic)s ", + "Add a topic to help people know what it is about.": "Add a topic to help people know what it is about.", + "You created this room.": "You created this room.", + "%(displayName)s created this room.": "%(displayName)s created this room.", + "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", + "This is the start of .": "This is the start of .", "No pinned messages.": "No pinned messages.", "Loading...": "Loading...", "Pinned Messages": "Pinned Messages", @@ -1633,8 +1642,9 @@ "Today": "Today", "Yesterday": "Yesterday", "View Source": "View Source", + "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.", "Encryption enabled": "Encryption enabled", - "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.", "Ignored attempt to disable encryption": "Ignored attempt to disable encryption", "Encryption not enabled": "Encryption not enabled", "The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.", @@ -1680,8 +1690,8 @@ "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", - "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", "Click here to see older messages.": "Click here to see older messages.", + "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", "Copied!": "Copied!", "Failed to copy": "Failed to copy", "Add an Integration": "Add an Integration", @@ -2331,6 +2341,7 @@ "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Self-verification request": "Self-verification request", "Logout": "Logout", + "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", @@ -2376,7 +2387,6 @@ "Starting microphone...": "Starting microphone...", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", - "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "Search failed": "Search failed", diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 235ae94010..f40f8c5187 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -38,6 +38,7 @@ import { configure, mount } from "enzyme"; import Velocity from 'velocity-animate'; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; import RoomContext from "../../../src/contexts/RoomContext"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; configure({ adapter: new Adapter() }); @@ -52,7 +53,7 @@ class WrappedMessagePanel extends React.Component { render() { return - + ; @@ -79,6 +80,8 @@ describe('MessagePanel', function() { // complete without this even if we mock the clock and tick it // what should be the correct amount of time). Velocity.mock = true; + + DMRoomMap.makeShared(); }); afterEach(function() { @@ -433,8 +436,8 @@ describe('MessagePanel', function() { const rm = res.find('.mx_RoomView_myReadMarker_container').getDOMNode(); const rows = res.find('.mx_RoomView_MessageList').children(); - expect(rows.length).toEqual(6); - expect(rm.previousSibling).toEqual(rows.at(4).getDOMNode()); + expect(rows.length).toEqual(7); // 6 events + the NewRoomIntro + expect(rm.previousSibling).toEqual(rows.at(5).getDOMNode()); // read marker should be hidden given props and at the last event expect(isReadMarkerVisible(rm)).toBeFalsy(); diff --git a/test/test-utils.js b/test/test-utils.js index d006eee823..c8e623b1d9 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -242,6 +242,7 @@ export function mkStubRoom(roomId = null) { setBlacklistUnverifiedDevices: jest.fn(), on: jest.fn(), removeListener: jest.fn(), + getDMInviter: jest.fn(), }; }