Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Show room intro at beginning of visible history
Browse files Browse the repository at this point in the 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 <robin@robin.town>
  • Loading branch information
robintown committed Apr 19, 2021
1 parent 50dd9da commit a09f08d
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 43 deletions.
2 changes: 1 addition & 1 deletion res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 9 additions & 3 deletions src/components/structures/MessagePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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(<RoomHistoryIntro key="roomhistoryintro" />);
}

ret.push(
<MemberEventListSummary key={key}
events={this.events}
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/TimelinePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,7 @@ class TimelinePanel extends React.Component {
highlightedEventId={this.props.highlightedEventId}
readMarkerEventId={this.state.readMarkerEventId}
readMarkerVisible={this.state.readMarkerVisible}
suppressFirstDateSeparator={this.state.canBackPaginate}
canBackPaginate={this.state.canBackPaginate && this.state.firstVisibleEventIndex === 0}
showUrlPreview={this.props.showUrlPreview}
showReadReceipts={this.props.showReadReceipts}
ourUserId={MatrixClientPeg.get().credentials.userId}
Expand Down
42 changes: 6 additions & 36 deletions src/components/views/rooms/NewRoomIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,12 @@ limitations under the License.
import React, {useContext} from "react";
import {EventType} from "matrix-js-sdk/src/@types/event";

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 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";
import SpaceStore from "../../../stores/SpaceStore";
import {showSpaceInvite} from "../../../utils/space";
Expand All @@ -36,7 +32,6 @@ const NewRoomIntro = () => {
const {room, roomId} = useContext(RoomContext);

const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
let body;
if (dmPartner) {
let caption;
if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
Expand All @@ -45,22 +40,12 @@ const NewRoomIntro = () => {

const member = room?.getMember(dmPartner);
const displayName = member?.rawDisplayName || dmPartner;
body = <React.Fragment>
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
member: member || {userId: dmPartner},
});
}} />

<h2>{ room.name }</h2>

return <RoomIntro>
<p>{_t("This is the beginning of your direct message history with <displayName/>.", {}, {
displayName: () => <b>{ displayName }</b>,
})}</p>
{ caption && <p>{ caption }</p> }
</React.Fragment>;
</RoomIntro>;
} else {
const inRoom = room && room.getMyMembership() === "join";
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
Expand Down Expand Up @@ -146,29 +131,14 @@ const NewRoomIntro = () => {
</div>;
}

const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
body = <React.Fragment>
<MiniAvatarUploader
hasAvatar={!!avatarUrl}
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
>
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} />
</MiniAvatarUploader>

<h2>{ room.name }</h2>

return <RoomIntro>
<p>{createdText} {_t("This is the start of <roomName/>.", {}, {
roomName: () => <b>{ room.name }</b>,
})}</p>
<p>{topicText}</p>
{ topicText && <p>{topicText}</p> }
{ buttons }
</React.Fragment>;
</RoomIntro>;
}

return <div className="mx_NewRoomIntro">
{ body }
</div>;
};

export default NewRoomIntro;
136 changes: 136 additions & 0 deletions src/components/views/rooms/RoomHistoryIntro.tsx
Original file line number Diff line number Diff line change
@@ -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 <displayName/>, "
+ "as encrypted messages before this point are unavailable.", {}, {
displayName: () => <b>{ displayName }</b>,
});
} else if (historyState == "invited") {
caption1 = _t("This is the beginning of your visible history with <displayName/>, "
+ "as the room's admins have restricted your ability to view messages "
+ "from before you were invited.", {}, {
displayName: () => <b>{ displayName }</b>,
});
} else if (historyState == "joined") {
caption1 = _t("This is the beginning of your visible history with <displayName/>, "
+ "as the room's admins have restricted your ability to view messages "
+ "from before you joined.", {}, {
displayName: () => <b>{ displayName }</b>,
});
} else {
caption1 = _t("This is the beginning of your visible history with <displayName/>.", {}, {
displayName: () => <b>{ displayName }</b>,
});
}

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 <RoomIntro>
<p>{ caption1 }</p>
{ caption2 && <p>{ caption2 }</p> }
</RoomIntro>;
} 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 (<a>edit</a>)", { topic }, {
a: sub => <AccessibleButton kind="link" onClick={onTopicClick}>{ sub }</AccessibleButton>,
});
} else if (topic) {
topicText = _t("Topic: %(topic)s ", { topic });
} else if (canAddTopic) {
topicText = _t("<a>Add a topic</a> to help people know what it is about.", {}, {
a: sub => <AccessibleButton kind="link" onClick={onTopicClick}>{ sub }</AccessibleButton>,
});
}

let caption;
if (encryptionState) {
caption = _t("This is the beginning of your visible history in <roomName/>, "
+ "as encrypted messages before this point are unavailable.", {}, {
roomName: () => <b>{ room.name }</b>,
});
} else if (historyState == "invited") {
caption = _t("This is the beginning of your visible history in <roomName/>, "
+ "as the room's admins have restricted your ability to view messages "
+ "from before you were invited.", {}, {
roomName: () => <b>{ room.name }</b>,
});
} else if (historyState == "joined") {
caption = _t("This is the beginning of your visible history in <roomName/>, "
+ "as the room's admins have restricted your ability to view messages "
+ "from before you joined.", {}, {
roomName: () => <b>{ room.name }</b>,
});
} else {
caption = _t("This is the beginning of your visible history in <roomName/>.", {}, {
roomName: () => <b>{ room.name }</b>,
});
}

return <RoomIntro>
<p>{ caption }</p>
{ topicText && <p>{ topicText }</p> }
</RoomIntro>;
}
};

export default RoomHistoryIntro;
65 changes: 65 additions & 0 deletions src/components/views/rooms/RoomIntro.tsx
Original file line number Diff line number Diff line change
@@ -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 = <RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
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 = (
<MiniAvatarUploader
hasAvatar={!!avatarUrl}
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
>
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} />
</MiniAvatarUploader>
);
}

return <div className="mx_RoomIntro">
{ avatar }
<h2>{ room.name }</h2>
{ children }
</div>;
};

export default RoomIntro;
10 changes: 9 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <roomName/>.": "This is the start of <roomName/>.",
"No pinned messages.": "No pinned messages.",
"Loading...": "Loading...",
Expand Down Expand Up @@ -1525,6 +1524,15 @@
"Search": "Search",
"Voice call": "Voice call",
"Video call": "Video call",
"This is the beginning of your visible history with <displayName/>, as encrypted messages before this point are unavailable.": "This is the beginning of your visible history with <displayName/>, as encrypted messages before this point are unavailable.",
"This is the beginning of your visible history with <displayName/>, 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 <displayName/>, 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 <displayName/>, 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 <displayName/>, 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 <displayName/>.": "This is the beginning of your visible history with <displayName/>.",
"This is the beginning of your visible history in <roomName/>, as encrypted messages before this point are unavailable.": "This is the beginning of your visible history in <roomName/>, as encrypted messages before this point are unavailable.",
"This is the beginning of your visible history in <roomName/>, 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 <roomName/>, 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 <roomName/>, 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 <roomName/>, 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 <roomName/>.": "This is the beginning of your visible history in <roomName/>.",
"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",
Expand Down

0 comments on commit a09f08d

Please sign in to comment.