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

Show a tile at beginning of visible history #5887

Merged
merged 27 commits into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b15c6cb
Show date separator at beginning of timeline with hidden undecryptable
robintown Apr 25, 2021
6430168
Factor resuable RoomIntro out of NewRoomIntro
robintown Apr 25, 2021
0063fd2
Show room intro at beginning of visible history
robintown Apr 25, 2021
a2c8907
Merge branch 'develop' into room-history-intro
robintown Apr 25, 2021
0592435
Don't need 2020 in copyright
robintown May 4, 2021
98fbb7c
Check history visibility before encryption in room history intro
robintown May 4, 2021
cff6f4d
Consolidate room intro message and button code
robintown May 4, 2021
2a561c1
Merge branch 'develop' into room-history-intro
robintown May 4, 2021
cb7506f
Merge branch 'develop' into room-history-intro
robintown May 24, 2021
a68d180
Merge branch 'develop' into room-history-intro
robintown Jun 12, 2021
721693c
Consistently rename NewRoomIntro things to RoomIntro
robintown Jun 12, 2021
ee83103
Make myself the copyright holder for room history intro code
robintown Jun 12, 2021
e1c7ee0
Fix i18n strings
robintown Jun 12, 2021
272b68f
Merge branch 'develop' into room-history-intro
robintown Jun 23, 2021
cf01987
Merge branch 'develop' into room-history-intro
robintown Jul 11, 2021
d65ae77
Merge branch 'develop' into room-history-intro
robintown Jul 21, 2021
9ea373b
Fix lints
robintown Jul 21, 2021
3ef6675
Merge branch 'develop' into room-history-intro
robintown Jul 24, 2021
160277c
Merge branch 'develop' into room-history-intro
robintown Aug 6, 2021
5ae4836
Merge branch 'develop' into room-history-intro
robintown Aug 26, 2021
d0b134c
Merge branch 'develop' into room-history-intro
robintown Sep 3, 2021
d2fe79e
Merge branch 'develop' into room-history-intro
robintown Sep 18, 2021
62a7f5a
Merge branch 'develop' into room-history-intro
robintown Oct 22, 2021
1dc47b8
Merge branch 'develop' into room-history-intro
robintown Oct 25, 2021
7d1c362
Merge branch 'develop' into room-history-intro
robintown Nov 25, 2021
eba604c
Merge branch 'develop' into room-history-intro
robintown Dec 17, 2021
68ef007
Convert room history notice to a tile
robintown Dec 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,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/_PresenceLabel.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 All @@ -28,7 +28,7 @@ limitations under the License.
font-size: inherit;
}

.mx_NewRoomIntro_buttons {
.mx_RoomIntro_buttons {
margin-top: 28px;

.mx_AccessibleButton {
Expand All @@ -53,7 +53,7 @@ limitations under the License.
}
}

.mx_NewRoomIntro_inviteButton::before {
.mx_RoomIntro_inviteButton::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
Expand Down
12 changes: 9 additions & 3 deletions src/components/structures/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { hasText } 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";
import defaultDispatcher from '../../dispatcher/dispatcher';
import CallEventGrouper from "./CallEventGrouper";
Expand Down Expand Up @@ -124,8 +125,8 @@ interface IProps {
// for pending messages.
ourUserId?: string;

// true to suppress the date at the start of the timeline
suppressFirstDateSeparator?: boolean;
// whether the timeline can visually go back any further
canBackPaginate?: boolean;

// whether to show read receipts
showReadReceipts?: boolean;
Expand Down Expand Up @@ -776,7 +777,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
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 @@ -1318,6 +1319,11 @@ class MemberGrouper extends BaseGrouper {
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}
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
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
165 changes: 5 additions & 160 deletions src/components/views/rooms/NewRoomIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,97 +16,27 @@ limitations under the License.

import React, { useContext } from "react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";

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";
import { privateShouldBeEncrypted } from "../../../createRoom";
import EventTileBubble from "../messages/EventTileBubble";
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";

function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
const isPublic: boolean = room.getJoinRule() === "public";
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
}

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 = <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 } as User,
});
}}
/>

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

const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
const creatorName = room?.getMember(creator)?.rawDisplayName || creator;

Expand All @@ -119,97 +49,12 @@ const NewRoomIntro = () => {
});
}

let parentSpace;
if (
SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) &&
SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
) {
parentSpace = SpaceStore.instance.activeSpace;
}

let buttons;
if (parentSpace) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
showSpaceInvite(parentSpace);
}}
>
{ _t("Invite to %(spaceName)s", { spaceName: parentSpace.name }) }
</AccessibleButton>
{ room.canInvite(cli.getUserId()) && <AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary_outline"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{ _t("Invite to just this room") }
</AccessibleButton> }
</div>;
} else if (room.canInvite(cli.getUserId())) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{ _t("Invite to this room") }
</AccessibleButton>
</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>
{ buttons }
</React.Fragment>;
</RoomIntro>;
}

function openRoomSettings(event) {
event.preventDefault();
dis.dispatch({
action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB,
});
}

const sub2 = _t(
"Your private messages are normally encrypted, but this room isn't. "+
"Usually this is due to an unsupported device or method being used, " +
"like email invites. <a>Enable encryption in settings.</a>", {},
{ a: sub => <a onClick={openRoomSettings} href="#">{ sub }</a> },
);

return <div className="mx_NewRoomIntro">

{ !hasExpectedEncryptionSettings(cli, room) && (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
title={_t("End-to-end encryption isn't enabled")}
subtitle={sub2}
/>
) }

{ body }
</div>;
};

export default NewRoomIntro;
90 changes: 90 additions & 0 deletions src/components/views/rooms/RoomHistoryIntro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright 2021 Robin Townsend <robin@robin.town>

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 { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";

import RoomIntro from "./RoomIntro";
import RoomContext from "../../../contexts/RoomContext";
import DMRoomMap from "../../../utils/DMRoomMap";
import { _t } from "../../../languageHandler";

const RoomHistoryIntro = () => {
const { room, roomId } = useContext(RoomContext);

const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
const encryptionState = oldState.getStateEvents("m.room.encryption")[0];
const historyState = oldState.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility;

let caption;
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (dmPartner) {
const member = room?.getMember(dmPartner);
const displayName = member?.rawDisplayName || dmPartner;

if (historyState == "invited") {
caption = _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") {
caption = _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 if (encryptionState) {
caption = _t("This is the beginning of your visible history with <displayName/>, "
+ "as encrypted messages before this point are unavailable.", {}, {
displayName: () => <b>{ displayName }</b>,
});
} else {
caption = _t("This is the beginning of your visible history with <displayName/>.", {}, {
displayName: () => <b>{ displayName }</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 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 {
caption = _t("This is the beginning of your visible history in <roomName/>.", {}, {
roomName: () => <b>{ room.name }</b>,
});
}
}

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

export default RoomHistoryIntro;
Loading