);
+ 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 ;
}
}
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
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