From ff25a9b45de5e5125bfb6552ff7470df8327acd1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Nov 2020 17:00:07 +0000 Subject: [PATCH 01/16] Consolidate all EventTile bubble stuff into its own component and use it for the room continuation plinth --- res/css/_components.scss | 1 + res/css/views/messages/_CreateEvent.scss | 34 ++++------ res/css/views/messages/_EventTileBubble.scss | 63 +++++++++++++++++++ .../views/messages/_MJitsiWidgetEvent.scss | 33 ---------- .../views/messages/_common_CryptoEvent.scss | 48 +++----------- res/css/views/rooms/_EventTile.scss | 9 --- .../views/messages/EncryptionEvent.js | 45 ++++++------- .../views/messages/EventTileBubble.tsx | 34 ++++++++++ .../views/messages/MJitsiWidgetEvent.tsx | 42 +++++-------- .../messages/MKeyVerificationConclusion.js | 13 ++-- .../views/messages/MKeyVerificationRequest.js | 25 ++++---- src/components/views/messages/RoomCreate.js | 20 +++--- src/components/views/rooms/EventTile.js | 1 + 13 files changed, 182 insertions(+), 186 deletions(-) create mode 100644 res/css/views/messages/_EventTileBubble.scss create mode 100644 src/components/views/messages/EventTileBubble.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 37d0e0d286..af3589a415 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -139,6 +139,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"; diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index d45645863f..1f75fb9693 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 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,17 @@ limitations under the License. */ .mx_CreateEvent { - background-color: $info-plinth-bg-color; - padding-left: 20px; - padding-right: 20px; - padding-top: 10px; - padding-bottom: 10px; -} + // override default EventTileBubble styling + padding-left: 80px !important; -.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: $primary-fg-color; + mask-image: url('$(res)/img/room-continuation.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 100%; + width: 72px !important; + height: 34px !important; + left: -64px !important; + } } diff --git a/res/css/views/messages/_EventTileBubble.scss b/res/css/views/messages/_EventTileBubble.scss new file mode 100644 index 0000000000..99feb03ef9 --- /dev/null +++ b/res/css/views/messages/_EventTileBubble.scss @@ -0,0 +1,63 @@ +/* +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 { + grid-column: 2; + grid-row: 2; + } + + .mx_EventTileBubble_subtitle { + font-size: $font-12px; + } +} 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..18eb581776 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; } diff --git a/src/components/views/messages/EncryptionEvent.js b/src/components/views/messages/EncryptionEvent.js index a9ce10d202..ee304b4fa3 100644 --- a/src/components/views/messages/EncryptionEvent.js +++ b/src/components/views/messages/EncryptionEvent.js @@ -18,42 +18,35 @@ import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import EventTileBubble from "./EventTileBubble"; 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.", - )} -
-
; + return ; } 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 ; } - return (
- {body} -
); + return ; } } diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx new file mode 100644 index 0000000000..8a2cf0f01c --- /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, {ReactNode} from "react"; +import classNames from "classnames"; + +interface IProps { + className: string; + title: string; + subtitle?: ReactNode; +} + +const EventTileBubble: React.FC = ({ className, title, subtitle, children }) => { + 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..9060ddb02b 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -647,6 +647,7 @@ 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.create") || (eventType === "m.room.encryption") || (tileHandler === "messages.MJitsiWidgetEvent"); let isInfoMessage = ( From 4e2d9c28f5347ac184492cf6ec6ac5433e492ba2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Nov 2020 17:21:25 +0000 Subject: [PATCH 02/16] Convert EncryptionEvent to Typescript --- .../views/messages/EncryptionEvent.js | 56 ------------------- .../views/messages/EncryptionEvent.tsx | 54 ++++++++++++++++++ 2 files changed, 54 insertions(+), 56 deletions(-) delete mode 100644 src/components/views/messages/EncryptionEvent.js create mode 100644 src/components/views/messages/EncryptionEvent.tsx diff --git a/src/components/views/messages/EncryptionEvent.js b/src/components/views/messages/EncryptionEvent.js deleted file mode 100644 index ee304b4fa3..0000000000 --- a/src/components/views/messages/EncryptionEvent.js +++ /dev/null @@ -1,56 +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'; -import EventTileBubble from "./EventTileBubble"; - -export default class EncryptionEvent extends React.Component { - render() { - const {mxEvent} = this.props; - - const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId()); - if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { - return ; - } else if (isRoomEncrypted) { - return ; - } - - return ; - } -} - -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..9a6af9446f --- /dev/null +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -0,0 +1,54 @@ +/* +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 {MatrixEvent} from "matrix-js-sdk/src/models/event"; + +import { _t } from '../../../languageHandler'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import EventTileBubble from "./EventTileBubble"; + +interface IProps { + mxEvent: MatrixEvent; +} + +const EncryptionEvent: React.FC = ({mxEvent}) => { + const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId()); + if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { + return ; + } else if (isRoomEncrypted) { + return ; + } + + return ; +} + +export default EncryptionEvent; From af71b0735dfc0f28d15040514f60ebac22cb7084 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Nov 2020 17:24:03 +0000 Subject: [PATCH 03/16] Change icon for the Create Event bubble tile --- res/css/views/messages/_CreateEvent.scss | 11 +---------- res/img/element-icons/chat-bubbles.svg | 11 +++++++++++ res/img/room-continuation.svg | 6 ------ 3 files changed, 12 insertions(+), 16 deletions(-) create mode 100644 res/img/element-icons/chat-bubbles.svg delete mode 100644 res/img/room-continuation.svg diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index 1f75fb9693..1181a80d10 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -15,17 +15,8 @@ limitations under the License. */ .mx_CreateEvent { - // override default EventTileBubble styling - padding-left: 80px !important; - &::before { background-color: $primary-fg-color; - mask-image: url('$(res)/img/room-continuation.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 100%; - width: 72px !important; - height: 34px !important; - left: -64px !important; + mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); } } 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 @@ - - - - - - From 2fdfe9346f962abdb350ae0731f8ba90ad428370 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Nov 2020 17:50:59 +0000 Subject: [PATCH 04/16] Update copy on the Encryption Enabled bubble tile and room creation ELS to cater better in general but specifically for DMs --- src/components/structures/MessagePanel.js | 14 ++++++++--- .../views/messages/EncryptionEvent.tsx | 25 ++++++++++++++----- src/i18n/strings/en_EN.json | 6 +++-- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index e2e3592536..9cf3eaaa9d 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -30,6 +30,7 @@ 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"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -952,15 +953,22 @@ 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 creator = ev.sender ? ev.sender.name : ev.getSender(); + if (DMRoomMap.shared().getUserIdForRoomId(ev.getRoomId())) { + summaryText = _t("%(creator)s created this DM.", { creator }); + } else { + summaryText = _t("%(creator)s created and configured the room.", { creator }); + } + ret.push( { eventTiles } , diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index 9a6af9446f..7416cc3c9a 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -14,27 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {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: React.FC = ({mxEvent}) => { - const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId()); + 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 ": "%(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", @@ -2079,6 +2080,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!", From 2594ff8e803b5eb6d9125ddad50698172bde0eee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:39:22 +0000 Subject: [PATCH 05/16] Remove old isAlone checker --- res/css/structures/_RoomStatusBar.scss | 10 ----- src/components/structures/RoomStatusBar.js | 30 +------------- src/components/structures/RoomView.tsx | 46 +++------------------- 3 files changed, 7 insertions(+), 79 deletions(-) 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/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 0cb4a5d305..8618e930d5 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 = ; From 0a42853a2534b410807933d5d529bd178eff93ad Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:39:37 +0000 Subject: [PATCH 06/16] Fix centering of bubble event tile --- res/css/views/rooms/_EventTile.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 18eb581776..80ede1152d 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -122,9 +122,9 @@ $left-gutter: 64px; grid-template-columns: 1fr 100px; .mx_EventTile_line { - margin-right: 0px; + margin-right: 0; grid-column: 1 / 3; - padding: 0; + padding: 0 !important; } .mx_EventTile_msgOption { From 6aeea3e38e0ba6f8b6a2467b293f69148ee7c2e4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:42:45 +0000 Subject: [PATCH 07/16] Extract MiniAvatarUploader into a reusable component --- res/css/_components.scss | 1 + res/css/structures/_HomePage.scss | 36 +------- .../views/elements/_MiniAvatarUploader.scss | 56 ++++++++++++ src/components/structures/HomePage.tsx | 60 +++---------- .../views/elements/MiniAvatarUploader.tsx | 90 +++++++++++++++++++ 5 files changed, 159 insertions(+), 84 deletions(-) create mode 100644 res/css/views/elements/_MiniAvatarUploader.scss create mode 100644 src/components/views/elements/MiniAvatarUploader.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index af3589a415..860b0f126e 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"; 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/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/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/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx new file mode 100644 index 0000000000..903826c3e6 --- /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); + }, 3_000); // show after 3 seconds + useTimeout(() => { + setShow(false); + }, 13_000); // 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; From 664079a12735f0bbeb36593d2da754dbdfda76f6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:50:42 +0000 Subject: [PATCH 08/16] Use MiniAvatarUploader for the new NewRoomIntro component --- res/css/_components.scss | 1 + res/css/views/messages/_CreateEvent.scss | 2 +- res/css/views/rooms/_NewRoomIntro.scss | 67 ++++++++++ src/components/structures/MessagePanel.js | 6 +- src/components/views/avatars/RoomAvatar.tsx | 5 +- src/components/views/rooms/NewRoomIntro.tsx | 131 ++++++++++++++++++++ 6 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 res/css/views/rooms/_NewRoomIntro.scss create mode 100644 src/components/views/rooms/NewRoomIntro.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 860b0f126e..62b5031a31 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -184,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/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index 1181a80d10..a61a261ff1 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_CreateEvent { &::before { - background-color: $primary-fg-color; + background-color: $composer-e2e-icon-color; mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); } } 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/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 9cf3eaaa9d..512ffb6c07 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -31,6 +31,7 @@ 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']; @@ -955,13 +956,16 @@ class CreationGrouper { 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(ev.getRoomId())) { + 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( { }; 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/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; From c269577037ca209266c2e95cd8d182e4932f86d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:51:36 +0000 Subject: [PATCH 09/16] i18n --- src/i18n/strings/en_EN.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8b976c144e..271b288c11 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1083,6 +1083,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", @@ -2126,7 +2135,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", From 81f1e1d8d713da3264722a58ba534fd430d4c859 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:54:21 +0000 Subject: [PATCH 10/16] delint --- res/css/views/messages/_EventTileBubble.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/res/css/views/messages/_EventTileBubble.scss b/res/css/views/messages/_EventTileBubble.scss index 99feb03ef9..e0f5d521cb 100644 --- a/res/css/views/messages/_EventTileBubble.scss +++ b/res/css/views/messages/_EventTileBubble.scss @@ -53,11 +53,8 @@ limitations under the License. } .mx_EventTileBubble_subtitle { + font-size: $font-12px; grid-column: 2; grid-row: 2; } - - .mx_EventTileBubble_subtitle { - font-size: $font-12px; - } } From 602232a524d0b2b17fd878b3949e999a1634ec86 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 16:08:36 +0000 Subject: [PATCH 11/16] Update res/css/views/messages/_CreateEvent.scss Co-authored-by: Travis Ralston --- res/css/views/messages/_CreateEvent.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index a61a261ff1..cb2bf841dd 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +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. From 54e41b5f32e37e43686f827a05f232905d03ed2d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 16:27:41 +0000 Subject: [PATCH 12/16] fix tests --- src/components/views/messages/EncryptionEvent.tsx | 7 ++++--- src/components/views/messages/EventTileBubble.tsx | 8 ++++---- src/contexts/RoomContext.ts | 1 - test/components/structures/MessagePanel-test.js | 3 +++ test/test-utils.js | 1 + 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index 7416cc3c9a..3af9c463c9 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useContext} from 'react'; +import React, {forwardRef, useContext} from 'react'; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { _t } from '../../../languageHandler'; @@ -27,7 +27,7 @@ interface IProps { mxEvent: MatrixEvent; } -const EncryptionEvent: React.FC = ({mxEvent}) => { +const EncryptionEvent = forwardRef(({mxEvent}, ref) => { const cli = useContext(MatrixClientContext); const roomId = mxEvent.getRoomId(); const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); @@ -61,7 +61,8 @@ const EncryptionEvent: React.FC = ({mxEvent}) => { className="mx_cryptoEvent mx_cryptoEvent_icon mx_cryptoEvent_icon_warning" title={_t("Encryption not enabled")} subtitle={_t("The encryption used by this room isn't supported.")} + ref={ref} />; -} +}); export default EncryptionEvent; diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx index 8a2cf0f01c..f797a97a3d 100644 --- a/src/components/views/messages/EventTileBubble.tsx +++ b/src/components/views/messages/EventTileBubble.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ReactNode} from "react"; +import React, {forwardRef, ReactNode} from "react"; import classNames from "classnames"; interface IProps { @@ -23,12 +23,12 @@ interface IProps { subtitle?: ReactNode; } -const EventTileBubble: React.FC = ({ className, title, subtitle, children }) => { - return
+const EventTileBubble = forwardRef(({ className, title, subtitle, children }, ref) => { + return
{ title }
{ subtitle &&
{ subtitle }
} { children }
; -}; +}); export default EventTileBubble; 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/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 235ae94010..0bcdfcd5e5 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() }); @@ -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() { 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(), }; } From ae2d9941ff26c4f8dc55124dbf1237c879332b67 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 16:54:25 +0000 Subject: [PATCH 13/16] fix more tests --- src/Skinner.js | 4 ++-- test/components/structures/MessagePanel-test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 0bcdfcd5e5..0cb7e2cc71 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -53,7 +53,7 @@ class WrappedMessagePanel extends React.Component { render() { return - + ; From 4997676f5d472462b0ddbabafb7aafb1044ea7e3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 16:57:51 +0000 Subject: [PATCH 14/16] fix last remaining broken test --- test/components/structures/MessagePanel-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 0cb7e2cc71..f40f8c5187 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -436,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(); From 252484fcd9c9d435c41ea7f6832af04ee94a4277 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 17:01:20 +0000 Subject: [PATCH 15/16] add key to make React happier --- src/components/structures/MessagePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 512ffb6c07..375545f819 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -964,7 +964,7 @@ class CreationGrouper { summaryText = _t("%(creator)s created and configured the room.", { creator }); } - ret.push(); + ret.push(); ret.push( Date: Tue, 10 Nov 2020 10:00:55 +0000 Subject: [PATCH 16/16] Iterate PR --- res/css/views/rooms/_EventTile.scss | 1 + src/components/views/elements/MiniAvatarUploader.tsx | 6 +++--- src/components/views/rooms/EventTile.js | 11 ++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 80ede1152d..429ac7ed4b 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -124,6 +124,7 @@ $left-gutter: 64px; .mx_EventTile_line { margin-right: 0; grid-column: 1 / 3; + // override default padding of mx_EventTile_line so that we can be centered padding: 0 !important; } diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 903826c3e6..b5e117b42a 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -39,14 +39,14 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva useTimeout(() => { setShow(true); - }, 3_000); // show after 3 seconds + }, 3000); // show after 3 seconds useTimeout(() => { setShow(false); - }, 13_000); // hide after being shown for 10 seconds + }, 13000); // hide after being shown for 10 seconds const uploadRef = useRef(); - const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; + const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel; return