From a09f08def0262ca0106e75b9934252b113586862 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 19 Apr 2021 14:34:16 -0400 Subject: [PATCH] Show room intro at beginning of visible history When the user has reached the beginning of visible history in a room, show them a room intro to explain why messages before that point are unavailable and reassure them that the timeline has loaded. Signed-off-by: Robin Townsend --- res/css/_components.scss | 2 +- .../{_NewRoomIntro.scss => _RoomIntro.scss} | 2 +- src/components/structures/MessagePanel.js | 12 +- src/components/structures/TimelinePanel.js | 2 +- src/components/views/rooms/NewRoomIntro.tsx | 42 +----- .../views/rooms/RoomHistoryIntro.tsx | 136 ++++++++++++++++++ src/components/views/rooms/RoomIntro.tsx | 65 +++++++++ src/i18n/strings/en_EN.json | 10 +- 8 files changed, 228 insertions(+), 43 deletions(-) rename res/css/views/rooms/{_NewRoomIntro.scss => _RoomIntro.scss} (99%) create mode 100644 src/components/views/rooms/RoomHistoryIntro.tsx create mode 100644 src/components/views/rooms/RoomIntro.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 253f97bf4247..c9aee8cfdeaa 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -196,7 +196,7 @@ @import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MessageComposer.scss"; @import "./views/rooms/_MessageComposerFormatBar.scss"; -@import "./views/rooms/_NewRoomIntro.scss"; +@import "./views/rooms/_RoomIntro.scss"; @import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PinnedEventsPanel.scss"; diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_RoomIntro.scss similarity index 99% rename from res/css/views/rooms/_NewRoomIntro.scss rename to res/css/views/rooms/_RoomIntro.scss index 9c2a428cb36d..cb428220844a 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_RoomIntro.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_NewRoomIntro { +.mx_RoomIntro { margin: 40px 0 48px 64px; .mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 132d9ab4c396..b28219e2653d 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -33,6 +33,7 @@ import {textForEvent} from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import DMRoomMap from "../../utils/DMRoomMap"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; +import RoomHistoryIntro from "../views/rooms/RoomHistoryIntro"; import {replaceableComponent} from "../../utils/replaceableComponent"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes @@ -106,8 +107,8 @@ export default class MessagePanel extends React.Component { // for pending messages. ourUserId: PropTypes.string, - // true to suppress the date at the start of the timeline - suppressFirstDateSeparator: PropTypes.bool, + // whether the timeline can go back any further + canBackPaginate: PropTypes.bool, // whether to show read receipts showReadReceipts: PropTypes.bool, @@ -672,7 +673,7 @@ export default class MessagePanel extends React.Component { if (prevEvent == null) { // first event in the panel: depends if we could back-paginate from // here. - return !this.props.suppressFirstDateSeparator; + return !this.props.canBackPaginate; } return wantsDateSeparator(prevEvent.getDate(), nextEventDate); } @@ -1220,6 +1221,11 @@ class MemberGrouper { eventTiles = null; } + // If a membership event is the start of visible history, show a room intro + if (!this.panel.props.canBackPaginate && !this.prevEvent) { + ret.push(); + } + ret.push( { const {room, roomId} = useContext(RoomContext); const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); - let body; if (dmPartner) { let caption; if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) { @@ -45,22 +40,12 @@ const NewRoomIntro = () => { 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 }

- + return

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

{ caption &&

{ caption }

} -
; + ; } else { const inRoom = room && room.getMyMembership() === "join"; const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; @@ -146,29 +131,14 @@ const NewRoomIntro = () => { ; } - const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; - body = - cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')} - > - - - -

{ room.name }

- + return

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

-

{topicText}

+ { topicText &&

{topicText}

} { buttons } -
; + ; } - - return
- { body } -
; }; export default NewRoomIntro; diff --git a/src/components/views/rooms/RoomHistoryIntro.tsx b/src/components/views/rooms/RoomHistoryIntro.tsx new file mode 100644 index 000000000000..e89c7b848174 --- /dev/null +++ b/src/components/views/rooms/RoomHistoryIntro.tsx @@ -0,0 +1,136 @@ +/* +Copyright 2020, 2021 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 {EventTimeline} from "matrix-js-sdk/src/models/event-timeline"; + +import RoomIntro from "./RoomIntro"; +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 dis from "../../../dispatcher/dispatcher"; + +const RoomHistoryIntro = () => { + const cli = useContext(MatrixClientContext); + const {room, roomId} = useContext(RoomContext); + + const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); + const encryptionState = oldState.getStateEvents("m.room.encryption")[0]; + let historyState = oldState.getStateEvents("m.room.history_visibility")[0]; + historyState = historyState && historyState.getContent().history_visibility; + + const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); + if (dmPartner) { + const member = room?.getMember(dmPartner); + const displayName = member?.rawDisplayName || dmPartner; + + let caption1; + if (encryptionState) { + caption1 = _t("This is the beginning of your visible history with , " + + "as encrypted messages before this point are unavailable.", {}, { + displayName: () => { displayName }, + }); + } else if (historyState == "invited") { + caption1 = _t("This is the beginning of your visible history with , " + + "as the room's admins have restricted your ability to view messages " + + "from before you were invited.", {}, { + displayName: () => { displayName }, + }); + } else if (historyState == "joined") { + caption1 = _t("This is the beginning of your visible history with , " + + "as the room's admins have restricted your ability to view messages " + + "from before you joined.", {}, { + displayName: () => { displayName }, + }); + } else { + caption1 = _t("This is the beginning of your visible history with .", {}, { + displayName: () => { displayName }, + }); + } + + let caption2; + if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) { + caption2 = _t("Only the two of you are in this conversation, unless either of you invites anyone to join."); + } + + return +

{ caption1 }

+ { caption2 &&

{ caption2 }

} +
; + } else { + const inRoom = room && room.getMyMembership() === "join"; + const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; + const canAddTopic = inRoom && 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 }, + }); + } + + let caption; + if (encryptionState) { + caption = _t("This is the beginning of your visible history in , " + + "as encrypted messages before this point are unavailable.", {}, { + roomName: () => { room.name }, + }); + } else if (historyState == "invited") { + caption = _t("This is the beginning of your visible history in , " + + "as the room's admins have restricted your ability to view messages " + + "from before you were invited.", {}, { + roomName: () => { room.name }, + }); + } else if (historyState == "joined") { + caption = _t("This is the beginning of your visible history in , " + + "as the room's admins have restricted your ability to view messages " + + "from before you joined.", {}, { + roomName: () => { room.name }, + }); + } else { + caption = _t("This is the beginning of your visible history in .", {}, { + roomName: () => { room.name }, + }); + } + + return +

{ caption }

+ { topicText &&

{ topicText }

} +
; + } +}; + +export default RoomHistoryIntro; diff --git a/src/components/views/rooms/RoomIntro.tsx b/src/components/views/rooms/RoomIntro.tsx new file mode 100644 index 000000000000..fba527715916 --- /dev/null +++ b/src/components/views/rooms/RoomIntro.tsx @@ -0,0 +1,65 @@ +/* +Copyright 2020, 2021 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 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"; + +const RoomIntro: React.FC<{}> = ({ children }) => { + const cli = useContext(MatrixClientContext); + const {room, roomId} = useContext(RoomContext); + + const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); + let avatar; + if (dmPartner) { + const member = room?.getMember(dmPartner); + avatar = { + 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}, + }); + }} />; + } else { + const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; + avatar = ( + cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')} + > + + + ); + } + + return
+ { avatar } +

{ room.name }

+ { children } +
; +}; + +export default RoomIntro; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3bb575415fd1..ce99876c348c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1487,7 +1487,6 @@ "You created this room.": "You created this room.", "%(displayName)s created this room.": "%(displayName)s created this room.", "Invite to just this room": "Invite to just 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...", @@ -1525,6 +1524,15 @@ "Search": "Search", "Voice call": "Voice call", "Video call": "Video call", + "This is the beginning of your visible history with , as encrypted messages before this point are unavailable.": "This is the beginning of your visible history with , as encrypted messages before this point are unavailable.", + "This is the beginning of your visible history with , as the room's admins have restricted your ability to view messages from before you were invited.": "This is the beginning of your visible history with , as the room's admins have restricted your ability to view messages from before you were invited.", + "This is the beginning of your visible history with , as the room's admins have restricted your ability to view messages from before you joined.": "This is the beginning of your visible history with , as the room's admins have restricted your ability to view messages from before you joined.", + "This is the beginning of your visible history with .": "This is the beginning of your visible history with .", + "This is the beginning of your visible history in , as encrypted messages before this point are unavailable.": "This is the beginning of your visible history in , as encrypted messages before this point are unavailable.", + "This is the beginning of your visible history in , as the room's admins have restricted your ability to view messages from before you were invited.": "This is the beginning of your visible history in , as the room's admins have restricted your ability to view messages from before you were invited.", + "This is the beginning of your visible history in , as the room's admins have restricted your ability to view messages from before you joined.": "This is the beginning of your visible history in , as the room's admins have restricted your ability to view messages from before you joined.", + "This is the beginning of your visible history in .": "This is the beginning of your visible history in .", + "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "Start a Conversation": "Start a Conversation", "Open dial pad": "Open dial pad", "Invites": "Invites",