Iterate pinned messages

This commit is contained in:
Michael Telatynski 2021-05-26 13:51:17 +01:00
parent fd74a946e0
commit 27ad90760d
7 changed files with 185 additions and 361 deletions

View file

@ -15,227 +15,21 @@ limitations under the License.
*/ */
.mx_PinnedMessagesCard { .mx_PinnedMessagesCard {
padding-top: 0;
.mx_BaseCard_header { .mx_BaseCard_header {
text-align: center; text-align: center;
margin-top: 20px; margin-top: 0;
border-bottom: 1px solid $menu-border-color;
h2 { > h2 {
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
font-size: $font-18px; font-size: $font-18px;
margin: 12px 0 4px; margin: 8px 0;
} }
.mx_RoomSummaryCard_alias { .mx_BaseCard_close {
font-size: $font-13px; margin-right: 6px;
color: $secondary-fg-color;
}
h2, .mx_RoomSummaryCard_alias {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-wrap;
}
.mx_RoomSummaryCard_avatar {
display: inline-flex;
.mx_RoomSummaryCard_e2ee {
display: inline-block;
position: relative;
width: 54px;
height: 54px;
border-radius: 50%;
background-color: #737d8c;
margin-top: -3px; // alignment
margin-left: -10px; // overlap
border: 3px solid $dark-panel-bg-color;
&::before {
content: '';
position: absolute;
top: 13px;
left: 13px;
height: 28px;
width: 28px;
mask-size: cover;
mask-repeat: no-repeat;
mask-position: center;
mask-image: url('$(res)/img/e2e/disabled.svg');
background-color: #ffffff;
}
}
.mx_RoomSummaryCard_e2ee_normal {
background-color: #424446;
&::before {
mask-image: url('$(res)/img/e2e/normal.svg');
}
}
.mx_RoomSummaryCard_e2ee_verified {
background-color: #0dbd8b;
&::before {
mask-image: url('$(res)/img/e2e/verified.svg');
}
}
.mx_RoomSummaryCard_e2ee_warning {
background-color: #ff4b55;
&::before {
mask-image: url('$(res)/img/e2e/warning.svg');
} }
} }
} }
}
.mx_RoomSummaryCard_aboutGroup {
.mx_RoomSummaryCard_Button {
padding-left: 44px;
&::before {
content: '';
position: absolute;
top: 8px;
left: 10px;
height: 24px;
width: 24px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $icon-button-color;
}
}
}
.mx_RoomSummaryCard_appsGroup {
.mx_RoomSummaryCard_Button {
// this button is special so we have to override some of the original styling
// as we will be applying it in its children
padding: 0;
height: auto;
color: $tertiary-fg-color;
.mx_RoomSummaryCard_icon_app {
padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding
text-overflow: ellipsis;
overflow: hidden;
.mx_BaseAvatar_image {
vertical-align: top;
margin-right: 12px;
}
span {
color: $primary-fg-color;
}
}
.mx_RoomSummaryCard_app_pinToggle,
.mx_RoomSummaryCard_app_options {
position: absolute;
top: 0;
height: 100%; // to give bigger interactive zone
width: 24px;
padding: 12px 4px;
box-sizing: border-box;
min-width: 24px; // prevent flexbox crushing
&:hover {
&::after {
content: '';
position: absolute;
height: 24px;
width: 24px;
top: 8px; // equal to padding-top of parent
left: 0;
border-radius: 12px;
background-color: rgba(141, 151, 165, 0.1);
}
}
&::before {
content: '';
position: absolute;
height: 16px;
width: 16px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 16px;
background-color: $icon-button-color;
}
}
.mx_RoomSummaryCard_app_pinToggle {
right: 24px;
&::before {
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
}
}
.mx_RoomSummaryCard_app_options {
right: 48px;
display: none;
&::before {
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
}
}
&.mx_RoomSummaryCard_Button_pinned {
&::after {
opacity: 0.2;
}
.mx_RoomSummaryCard_app_pinToggle::before {
background-color: $accent-color;
}
}
&:hover {
.mx_RoomSummaryCard_icon_app {
padding-right: 72px;
}
.mx_RoomSummaryCard_app_options {
display: unset;
}
}
&::before {
content: unset;
}
&::after {
top: 8px; // re-align based on the height change
pointer-events: none; // pass through to the real button
}
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
margin-top: 12px;
margin-bottom: 12px;
font-size: $font-13px;
font-weight: $font-semi-bold;
}
}
.mx_RoomSummaryCard_icon_people::before {
mask-image: url("$(res)/img/element-icons/room/members.svg");
}
.mx_RoomSummaryCard_icon_files::before {
mask-image: url('$(res)/img/element-icons/room/files.svg');
}
.mx_RoomSummaryCard_icon_share::before {
mask-image: url('$(res)/img/element-icons/room/share.svg');
}
.mx_RoomSummaryCard_icon_settings::before {
mask-image: url('$(res)/img/element-icons/settings.svg');
}

View file

@ -16,62 +16,90 @@ limitations under the License.
.mx_PinnedEventTile { .mx_PinnedEventTile {
min-height: 40px; min-height: 40px;
margin-bottom: 5px;
width: 100%; width: 100%;
border-radius: 5px; // for the hover padding: 0 4px 12px;
display: grid;
grid-template-areas: "avatar name remove"
"content content content"
"footer footer footer";
grid-template-rows: max-content auto max-content;
grid-template-columns: 24px auto 24px;
grid-row-gap: 12px;
grid-column-gap: 8px;
& + .mx_PinnedEventTile {
padding: 12px 4px;
border-top: 1px solid $menu-border-color;
} }
.mx_PinnedEventTile:hover { .mx_PinnedEventTile_senderAvatar {
background-color: $event-selected-color; grid-area: avatar;
} }
.mx_PinnedEventTile .mx_PinnedEventTile_sender, .mx_PinnedEventTile_sender {
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { grid-area: name;
color: #868686; font-weight: $font-semi-bold;
font-size: 0.8em; font-size: $font-15px;
vertical-align: top; line-height: $font-24px;
display: inline-block; text-overflow: ellipsis;
padding-bottom: 3px; overflow: hidden;
} white-space: nowrap;
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
padding-left: 15px;
display: none;
}
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
float: left;
margin-right: 10px;
}
.mx_PinnedEventTile_actions {
float: right;
margin-right: 10px;
display: none;
}
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
display: inline-block;
}
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
display: block;
} }
.mx_PinnedEventTile_unpinButton { .mx_PinnedEventTile_unpinButton {
display: inline-block; visibility: hidden;
cursor: pointer; grid-area: remove;
margin-left: 10px; position: relative;
width: 24px;
height: 24px;
border-radius: 8px;
&:hover {
background-color: $roomheader-addroom-bg-color;
} }
.mx_PinnedEventTile_gotoButton { &::before {
display: inline-block; content: "";
font-size: 0.7em; // Smaller text to avoid conflicting with the layout position: absolute;
//top: 0;
//left: 0;
height: inherit;
width: inherit;
background: $secondary-fg-color;
mask-position: center;
mask-size: 8px;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/image-view/close.svg');
}
} }
.mx_PinnedEventTile_message { .mx_PinnedEventTile_message {
margin-left: 50px; grid-area: content;
position: relative; }
top: 0;
left: 0; .mx_PinnedEventTile_footer {
grid-area: footer;
font-size: 10px;
line-height: 12px;
.mx_PinnedEventTile_timestamp {
font-size: inherit;
line-height: inherit;
color: $secondary-fg-color;
}
.mx_AccessibleButton_kind_link {
padding: 0;
margin-left: 12px;
font-size: inherit;
line-height: inherit;
}
}
&:hover {
.mx_PinnedEventTile_unpinButton {
visibility: visible;
}
}
} }

View file

@ -16,6 +16,7 @@ limitations under the License.
import React, {useCallback, useContext, useEffect, useState} from "react"; import React, {useCallback, useContext, useEffect, useState} from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from 'matrix-js-sdk/src/@types/event'; import { EventType } from 'matrix-js-sdk/src/@types/event';
@ -42,7 +43,7 @@ export const usePinnedEvents = (room: Room): string[] => {
setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []); setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []);
}, [room]); }, [room]);
useEventEmitter(room.currentState, "RoomState.events", update); useEventEmitter(room?.currentState, "RoomState.events", update);
useEffect(() => { useEffect(() => {
update(); update();
return () => { return () => {
@ -53,7 +54,6 @@ export const usePinnedEvents = (room: Room): string[] => {
}; };
const ReadPinsEventId = "im.vector.room.read_pins"; const ReadPinsEventId = "im.vector.room.read_pins";
const ReadPinsNumIds = 10;
export const useReadPinnedEvents = (room: Room): Set<string> => { export const useReadPinnedEvents = (room: Room): Set<string> => {
const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set()); const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set());
@ -75,20 +75,36 @@ export const useReadPinnedEvents = (room: Room): Set<string> => {
return readPinnedEvents; return readPinnedEvents;
}; };
const useRoomState = <T extends any>(room: Room, mapper: (state: RoomState) => T): T => {
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
const update = useCallback(() => {
if (!room) return;
setValue(mapper(room.currentState));
}, [room, mapper]);
useEventEmitter(room?.currentState, "RoomState.events", update);
useEffect(() => {
update();
return () => {
setValue(undefined);
};
}, [update]);
return value;
};
const PinnedMessagesCard = ({ room, onClose }: IProps) => { const PinnedMessagesCard = ({ room, onClose }: IProps) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
const pinnedEventIds = usePinnedEvents(room); const pinnedEventIds = usePinnedEvents(room);
const readPinnedEvents = useReadPinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room);
useEffect(() => { useEffect(() => {
const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id)); const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id));
if (newlyRead.length > 0) { if (newlyRead.length > 0) {
// Only keep the last N event IDs to avoid infinite growth // clear out any read pinned events which no longer are pinned
cli.setRoomAccountData(room.roomId, ReadPinsEventId, { cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
event_ids: [ event_ids: pinnedEventIds,
...newlyRead.reverse(),
...readPinnedEvents,
].splice(0, ReadPinsNumIds),
}); });
} }
}, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]);
@ -122,24 +138,35 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
if (!pinnedEvents) { if (!pinnedEvents) {
content = <Spinner />; content = <Spinner />;
} else if (pinnedEvents.length > 0) { } else if (pinnedEvents.length > 0) {
content = pinnedEvents.filter(Boolean).map(ev => ( let onUnpinClicked;
<PinnedEventTile if (canUnpin) {
key={ev.getId()} onUnpinClicked = async (event: MatrixEvent) => {
mxRoom={room} const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
mxEvent={ev} if (pinnedEvents?.getContent()?.pinned) {
onUnpinned={() => {}} const pinned = pinnedEvents.getContent().pinned;
/> const index = pinned.indexOf(event.getId());
if (index !== -1) {
pinned.splice(index, 1);
await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, "");
}
}
};
}
// show them in reverse, with latest pinned at the top
content = pinnedEvents.filter(Boolean).reverse().map(ev => (
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={onUnpinClicked} />
)); ));
} else { } else {
content = <div className="mx_RightPanel_empty mx_NotificationPanel_empty"> content = <div className="mx_RightPanel_empty mx_PinnedMessagesCard_empty">
<h2>{_t("Youre all caught up")}</h2> <h2>{_t("Youre all caught up")}</h2>
<p>{_t("You have no visible notifications.")}</p> <p>{_t("You have no visible notifications.")}</p>
</div>; </div>;
} }
return <BaseCard return <BaseCard
header={<h2>{ _t("Pinned") }</h2>} header={<h2>{ _t("Pinned messages") }</h2>}
className="mx_NotificationPanel" className="mx_PinnedMessagesCard"
onClose={onClose} onClose={onClose}
> >
{ content } { content }

View file

@ -55,7 +55,7 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
return <HeaderButton return <HeaderButton
name="pinnedMessagesButton" name="pinnedMessagesButton"
title={_t("Pinned Messages")} title={_t("Pinned messages")}
isHighlighted={isHighlighted} isHighlighted={isHighlighted}
onClick={onClick} onClick={onClick}
analytics={["Right Panel", "Pinned Messages Button", "click"]} analytics={["Right Panel", "Pinned Messages Button", "click"]}

View file

@ -19,110 +19,86 @@ import React from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent"; import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar"; import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {formatFullDate} from '../../../DateUtils'; import { formatDate } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps { interface IProps {
mxRoom: Room; room: Room;
mxEvent: MatrixEvent; event: MatrixEvent;
onUnpinned?(): void; onUnpinClicked?(): void;
} }
const AVATAR_SIZE = 24;
@replaceableComponent("views.rooms.PinnedEventTile") @replaceableComponent("views.rooms.PinnedEventTile")
export default class PinnedEventTile extends React.Component<IProps> { export default class PinnedEventTile extends React.Component<IProps> {
static contextType = MatrixClientContext;
private onTileClicked = () => { private onTileClicked = () => {
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
event_id: this.props.mxEvent.getId(), event_id: this.props.event.getId(),
highlighted: true, highlighted: true,
room_id: this.props.mxEvent.getRoomId(), room_id: this.props.event.getRoomId(),
}); });
}; };
private onUnpinClicked = () => {
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
// Nothing to do: already unpinned
if (this.props.onUnpinned) this.props.onUnpinned();
} else {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(this.props.mxEvent.getId());
if (index !== -1) {
pinned.splice(index, 1);
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
.then(() => {
if (this.props.onUnpinned) this.props.onUnpinned();
});
} else if (this.props.onUnpinned) this.props.onUnpinned();
}
};
private canUnpin() {
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
}
render() { render() {
const sender = this.props.mxEvent.getSender(); const sender = this.props.event.getSender();
// Get the latest sender profile rather than historical const senderProfile = this.props.room.getMember(sender);
const senderProfile = this.props.mxRoom.getMember(sender);
const avatarSize = 40;
let unpinButton = null; let unpinButton = null;
if (this.canUnpin()) { if (this.props.onUnpinClicked) {
unpinButton = ( unpinButton = (
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton"> <AccessibleTooltipButton
<img onClick={this.props.onUnpinClicked}
src={require("../../../../res/img/cancel-red.svg")} className="mx_PinnedEventTile_unpinButton"
width="8" title={_t("Unpin")}
height="8"
alt={_t('Unpin Message')}
title={_t('Unpin Message')}
/> />
</AccessibleButton>
); );
} }
return ( return <div className="mx_PinnedEventTile">
<div className="mx_PinnedEventTile">
<div className="mx_PinnedEventTile_actions">
<AccessibleButton
className="mx_PinnedEventTile_gotoButton mx_textButton"
onClick={this.onTileClicked}
>
{ _t("Jump to message") }
</AccessibleButton>
{ unpinButton }
</div>
<span className="mx_PinnedEventTile_senderAvatar">
<MemberAvatar <MemberAvatar
className="mx_PinnedEventTile_senderAvatar"
member={senderProfile} member={senderProfile}
width={avatarSize} width={AVATAR_SIZE}
height={avatarSize} height={AVATAR_SIZE}
fallbackUserId={sender} fallbackUserId={sender}
/> />
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
{ senderProfile?.name || sender }
</span> </span>
<span className="mx_PinnedEventTile_sender">
{ senderProfile ? senderProfile.name : sender } { unpinButton }
</span>
<span className="mx_PinnedEventTile_timestamp">
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
</span>
<div className="mx_PinnedEventTile_message"> <div className="mx_PinnedEventTile_message">
<MessageEvent <MessageEvent
mxEvent={this.props.mxEvent} mxEvent={this.props.event}
className="mx_PinnedEventTile_body" className="mx_PinnedEventTile_body"
maxImageHeight={150} maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently onHeightChanged={() => {}} // we need to give this, apparently
/> />
</div> </div>
<div className="mx_PinnedEventTile_footer">
<span className="mx_PinnedEventTile_timestamp">
{ formatDate(new Date(this.props.event.getTs())) }
</span>
<AccessibleButton onClick={this.onTileClicked} kind="link">
{ _t("View message") }
</AccessibleButton>
</div> </div>
); </div>;
} }
} }

View file

@ -1509,7 +1509,7 @@
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your 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/>.", "This is the start of <roomName/>.": "This is the start of <roomName/>.",
"Unpin Message": "Unpin Message", "Unpin Message": "Unpin Message",
"Jump to message": "Jump to message", "View message": "View message",
"%(duration)ss": "%(duration)ss", "%(duration)ss": "%(duration)ss",
"%(duration)sm": "%(duration)sm", "%(duration)sm": "%(duration)sm",
"%(duration)sh": "%(duration)sh", "%(duration)sh": "%(duration)sh",
@ -1717,8 +1717,7 @@
"Yours, or the other users session": "Yours, or the other users session", "Yours, or the other users session": "Yours, or the other users session",
"Youre all caught up": "Youre all caught up", "Youre all caught up": "Youre all caught up",
"You have no visible notifications.": "You have no visible notifications.", "You have no visible notifications.": "You have no visible notifications.",
"Pinned": "Pinned", "Pinned messages": "Pinned messages",
"Pinned Messages": "Pinned Messages",
"Room Info": "Room Info", "Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
"Unpin": "Unpin", "Unpin": "Unpin",
@ -1952,7 +1951,6 @@
"Rotate Right": "Rotate Right", "Rotate Right": "Rotate Right",
"Download": "Download", "Download": "Download",
"Information": "Information", "Information": "Information",
"View message": "View message",
"Language Dropdown": "Language Dropdown", "Language Dropdown": "Language Dropdown",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times",

View file

@ -44,6 +44,7 @@ export enum RightPanelPhases {
export const RIGHT_PANEL_PHASES_NO_ARGS = [ export const RIGHT_PANEL_PHASES_NO_ARGS = [
RightPanelPhases.RoomSummary, RightPanelPhases.RoomSummary,
RightPanelPhases.NotificationPanel, RightPanelPhases.NotificationPanel,
RightPanelPhases.PinnedMessages,
RightPanelPhases.FilePanel, RightPanelPhases.FilePanel,
RightPanelPhases.RoomMemberList, RightPanelPhases.RoomMemberList,
RightPanelPhases.GroupMemberList, RightPanelPhases.GroupMemberList,