Initial cut of Pinned event card in the right panel
This commit is contained in:
parent
4fa6d3599b
commit
59f4c728c9
13 changed files with 483 additions and 152 deletions
|
@ -179,6 +179,7 @@
|
||||||
@import "./views/messages/_common_CryptoEvent.scss";
|
@import "./views/messages/_common_CryptoEvent.scss";
|
||||||
@import "./views/right_panel/_BaseCard.scss";
|
@import "./views/right_panel/_BaseCard.scss";
|
||||||
@import "./views/right_panel/_EncryptionInfo.scss";
|
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||||
|
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
||||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||||
@import "./views/right_panel/_UserInfo.scss";
|
@import "./views/right_panel/_UserInfo.scss";
|
||||||
@import "./views/right_panel/_VerificationPanel.scss";
|
@import "./views/right_panel/_VerificationPanel.scss";
|
||||||
|
@ -203,7 +204,6 @@
|
||||||
@import "./views/rooms/_NewRoomIntro.scss";
|
@import "./views/rooms/_NewRoomIntro.scss";
|
||||||
@import "./views/rooms/_NotificationBadge.scss";
|
@import "./views/rooms/_NotificationBadge.scss";
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
|
||||||
@import "./views/rooms/_PresenceLabel.scss";
|
@import "./views/rooms/_PresenceLabel.scss";
|
||||||
@import "./views/rooms/_ReplyPreview.scss";
|
@import "./views/rooms/_ReplyPreview.scss";
|
||||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||||
|
|
|
@ -98,6 +98,48 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dot-size: 8px;
|
||||||
|
$pulse-color: $pinned-unread-color;
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton_unreadIndicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 4px;
|
||||||
|
width: $dot-size;
|
||||||
|
height: $dot-size;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(1);
|
||||||
|
background: rgba($pulse-color, 1);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
||||||
|
animation: mx_RightPanel_indicator_pulse 2s infinite;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mx_RightPanel_indicator_pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RightPanel_headerButton_highlight {
|
.mx_RightPanel_headerButton_highlight {
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $accent-color !important;
|
background-color: $accent-color !important;
|
||||||
|
|
241
res/css/views/right_panel/_PinnedMessagesCard.scss
Normal file
241
res/css/views/right_panel/_PinnedMessagesCard.scss
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_PinnedMessagesCard {
|
||||||
|
.mx_BaseCard_header {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-18px;
|
||||||
|
margin: 12px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSummaryCard_alias {
|
||||||
|
font-size: $font-13px;
|
||||||
|
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');
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Travis Ralston
|
|
||||||
|
|
||||||
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_PinnedEventsPanel {
|
|
||||||
border-top: 1px solid $primary-hairline-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_body {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_header {
|
|
||||||
margin: 0;
|
|
||||||
padding-top: 8px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_cancel {
|
|
||||||
margin: 12px;
|
|
||||||
float: right;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
|
@ -277,45 +277,6 @@ limitations under the License.
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_pinnedButton::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
$dot-size: 8px;
|
|
||||||
$pulse-color: $pinned-unread-color;
|
|
||||||
|
|
||||||
.mx_RoomHeader_pinsIndicatorUnread {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
margin: 4px;
|
|
||||||
width: $dot-size;
|
|
||||||
height: $dot-size;
|
|
||||||
border-radius: 50%;
|
|
||||||
transform: scale(1);
|
|
||||||
background: rgba($pulse-color, 1);
|
|
||||||
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
|
||||||
animation: mx_RoomHeader_indicator_pulse 2s infinite;
|
|
||||||
animation-iteration-count: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes mx_RoomHeader_indicator_pulse {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.95);
|
|
||||||
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
transform: scale(1);
|
|
||||||
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: scale(0.95);
|
|
||||||
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
.mx_RoomHeader_wrapper {
|
.mx_RoomHeader_wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -26,9 +26,9 @@ import dis from '../../dispatcher/dispatcher';
|
||||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||||
import GroupStore from '../../stores/GroupStore';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
import {
|
import {
|
||||||
RightPanelPhases,
|
|
||||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||||
RIGHT_PANEL_SPACE_PHASES,
|
RIGHT_PANEL_SPACE_PHASES,
|
||||||
|
RightPanelPhases,
|
||||||
} from "../../stores/RightPanelStorePhases";
|
} from "../../stores/RightPanelStorePhases";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
@ -47,6 +47,7 @@ import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
|
||||||
import FilePanel from "./FilePanel";
|
import FilePanel from "./FilePanel";
|
||||||
import NotificationPanel from "./NotificationPanel";
|
import NotificationPanel from "./NotificationPanel";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room?: Room; // if showing panels for a given room, this is set
|
room?: Room; // if showing panels for a given room, this is set
|
||||||
|
@ -294,7 +295,13 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RightPanelPhases.NotificationPanel:
|
case RightPanelPhases.NotificationPanel:
|
||||||
panel = <NotificationPanel onClose={this.onClose} />;
|
if (SettingsStore.getValue("feature_pinning")) {
|
||||||
|
panel = <NotificationPanel onClose={this.onClose} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RightPanelPhases.PinnedMessages:
|
||||||
|
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RightPanelPhases.FilePanel:
|
case RightPanelPhases.FilePanel:
|
||||||
|
|
|
@ -155,7 +155,6 @@ export interface IState {
|
||||||
canPeek: boolean;
|
canPeek: boolean;
|
||||||
showApps: boolean;
|
showApps: boolean;
|
||||||
isPeeking: boolean;
|
isPeeking: boolean;
|
||||||
showingPinned: boolean;
|
|
||||||
showReadReceipts: boolean;
|
showReadReceipts: boolean;
|
||||||
showRightPanel: boolean;
|
showRightPanel: boolean;
|
||||||
// error object, as from the matrix client/server API
|
// error object, as from the matrix client/server API
|
||||||
|
@ -232,7 +231,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showingPinned: false,
|
|
||||||
showReadReceipts: true,
|
showReadReceipts: true,
|
||||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||||
joining: false,
|
joining: false,
|
||||||
|
@ -327,7 +325,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||||
// we should only peek once we have a ready client
|
// we should only peek once we have a ready client
|
||||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||||
};
|
};
|
||||||
|
@ -1375,13 +1372,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPinnedClick = () => {
|
|
||||||
const nowShowingPinned = !this.state.showingPinned;
|
|
||||||
const roomId = this.state.room.roomId;
|
|
||||||
this.setState({showingPinned: nowShowingPinned, searching: false});
|
|
||||||
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCallPlaced = (type: PlaceCallType) => {
|
private onCallPlaced = (type: PlaceCallType) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
|
@ -1498,7 +1488,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
private onSearchClick = () => {
|
private onSearchClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searching: !this.state.searching,
|
searching: !this.state.searching,
|
||||||
showingPinned: false,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1825,9 +1814,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
} else if (showRoomUpgradeBar) {
|
} else if (showRoomUpgradeBar) {
|
||||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||||
hideCancel = true;
|
hideCancel = true;
|
||||||
} else if (this.state.showingPinned) {
|
|
||||||
hideCancel = true; // has own cancel
|
|
||||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
|
||||||
} else if (myMembership !== "join") {
|
} else if (myMembership !== "join") {
|
||||||
// We do have a room object for this room, but we're not currently in it.
|
// We do have a room object for this room, but we're not currently in it.
|
||||||
// We may have a 3rd party invite to it.
|
// We may have a 3rd party invite to it.
|
||||||
|
@ -2045,7 +2031,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
inRoom={myMembership === 'join'}
|
inRoom={myMembership === 'join'}
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
onPinnedClick={this.onPinnedClick}
|
|
||||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
|
|
145
src/components/views/right_panel/PinnedMessagesCard.tsx
Normal file
145
src/components/views/right_panel/PinnedMessagesCard.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
Copyright 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, {useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import BaseCard from "./BaseCard";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
|
import PinningUtils from "../../../utils/PinningUtils";
|
||||||
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
|
import PinnedEventTile from "../rooms/PinnedEventTile";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room: Room;
|
||||||
|
onClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePinnedEvents = (room: Room): string[] => {
|
||||||
|
const [pinnedEvents, setPinnedEvents] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const update = useCallback((ev?: MatrixEvent) => {
|
||||||
|
if (!room) return;
|
||||||
|
if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
|
||||||
|
setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []);
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
|
useEventEmitter(room.currentState, "RoomState.events", update);
|
||||||
|
useEffect(() => {
|
||||||
|
update();
|
||||||
|
return () => {
|
||||||
|
setPinnedEvents([]);
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
return pinnedEvents;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReadPinsEventId = "im.vector.room.read_pins";
|
||||||
|
const ReadPinsNumIds = 10;
|
||||||
|
|
||||||
|
export const useReadPinnedEvents = (room: Room): Set<string> => {
|
||||||
|
const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const update = useCallback((ev?: MatrixEvent) => {
|
||||||
|
if (!room) return;
|
||||||
|
if (ev && ev.getType() !== ReadPinsEventId) return;
|
||||||
|
const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids;
|
||||||
|
setReadPinnedEvents(new Set(readPins || []));
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
|
useEventEmitter(room, "Room.accountData", update);
|
||||||
|
useEffect(() => {
|
||||||
|
update();
|
||||||
|
return () => {
|
||||||
|
setReadPinnedEvents(new Set());
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
return readPinnedEvents;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const pinnedEventIds = usePinnedEvents(room);
|
||||||
|
const readPinnedEvents = useReadPinnedEvents(room);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id));
|
||||||
|
if (newlyRead.length > 0) {
|
||||||
|
// Only keep the last N event IDs to avoid infinite growth
|
||||||
|
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||||
|
event_ids: [
|
||||||
|
...newlyRead.reverse(),
|
||||||
|
...readPinnedEvents,
|
||||||
|
].splice(0, ReadPinsNumIds),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [cli, room.roomId, pinnedEventIds, readPinnedEvents]);
|
||||||
|
|
||||||
|
const pinnedEvents = useAsyncMemo(() => {
|
||||||
|
const promises = pinnedEventIds.map(async eventId => {
|
||||||
|
const timelineSet = room.getUnfilteredTimelineSet();
|
||||||
|
const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId);
|
||||||
|
if (localEvent) return localEvent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const evJson = await cli.fetchRoomEvent(room.roomId, eventId);
|
||||||
|
const event = new MatrixEvent(evJson);
|
||||||
|
if (event.isEncrypted()) {
|
||||||
|
await cli.decryptEventIfNeeded(event); // TODO await?
|
||||||
|
}
|
||||||
|
if (event && PinningUtils.isPinnable(event)) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error looking up pinned event " + eventId + " in room " + room.roomId);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, [cli, room, pinnedEventIds], null);
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (!pinnedEvents) {
|
||||||
|
content = <Spinner />;
|
||||||
|
} else if (pinnedEvents.length > 0) {
|
||||||
|
content = pinnedEvents.filter(Boolean).map(ev => (
|
||||||
|
<PinnedEventTile
|
||||||
|
key={ev.getId()}
|
||||||
|
mxRoom={room}
|
||||||
|
mxEvent={ev}
|
||||||
|
onUnpinned={() => {}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
content = <div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||||
|
<h2>{_t("You’re all caught up")}</h2>
|
||||||
|
<p>{_t("You have no visible notifications.")}</p>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseCard className="mx_NotificationPanel" onClose={onClose} withoutScrollContainer>
|
||||||
|
{ content }
|
||||||
|
</BaseCard>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PinnedMessagesCard;
|
|
@ -18,7 +18,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import {_t} from '../../../languageHandler';
|
import {_t} from '../../../languageHandler';
|
||||||
import HeaderButton from './HeaderButton';
|
import HeaderButton from './HeaderButton';
|
||||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
||||||
|
@ -27,6 +29,8 @@ import {Action} from "../../../dispatcher/actions";
|
||||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {useSettingValue} from "../../../hooks/useSettings";
|
||||||
|
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
||||||
|
|
||||||
const ROOM_INFO_PHASES = [
|
const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.RoomSummary,
|
RightPanelPhases.RoomSummary,
|
||||||
|
@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.Room3pidMemberInfo,
|
RightPanelPhases.Room3pidMemberInfo,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
|
||||||
|
const pinningEnabled = useSettingValue("feature_pinning");
|
||||||
|
const pinnedEvents = usePinnedEvents(pinningEnabled && room);
|
||||||
|
const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
|
||||||
|
if (!pinningEnabled) return null;
|
||||||
|
|
||||||
|
let unreadIndicator;
|
||||||
|
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
|
||||||
|
unreadIndicator = <div className="mx_RightPanel_pinnedMessagesButton_unreadIndicator" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <HeaderButton
|
||||||
|
name="pinnedMessagesButton"
|
||||||
|
title={_t("Pinned Messages")}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
|
onClick={onClick}
|
||||||
|
analytics={["Right Panel", "Pinned Messages Button", "click"]}
|
||||||
|
>
|
||||||
|
{ unreadIndicator }
|
||||||
|
</HeaderButton>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room?: Room;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.right_panel.RoomHeaderButtons")
|
@replaceableComponent("views.right_panel.RoomHeaderButtons")
|
||||||
export default class RoomHeaderButtons extends HeaderButtons {
|
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props, HeaderKind.Room);
|
super(props, HeaderKind.Room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +110,18 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
||||||
this.setPhase(RightPanelPhases.NotificationPanel);
|
this.setPhase(RightPanelPhases.NotificationPanel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onPinnedMessagesClicked = () => {
|
||||||
|
// This toggles for us, if needed
|
||||||
|
this.setPhase(RightPanelPhases.PinnedMessages);
|
||||||
|
};
|
||||||
|
|
||||||
public renderButtons() {
|
public renderButtons() {
|
||||||
return <>
|
return <>
|
||||||
|
<PinnedMessagesHeaderButton
|
||||||
|
room={this.props.room}
|
||||||
|
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||||
|
onClick={this.onPinnedMessagesClicked}
|
||||||
|
/>
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
name="notifsButton"
|
name="notifsButton"
|
||||||
title={_t('Notifications')}
|
title={_t('Notifications')}
|
||||||
|
|
|
@ -514,9 +514,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||||
} else {
|
} else {
|
||||||
setPowerLevels({});
|
setPowerLevels({});
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
setPowerLevels({});
|
|
||||||
};
|
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
useEventEmitter(cli, "RoomState.events", update);
|
useEventEmitter(cli, "RoomState.events", update);
|
||||||
|
|
|
@ -40,7 +40,6 @@ export default class RoomHeader extends React.Component {
|
||||||
oobData: PropTypes.object,
|
oobData: PropTypes.object,
|
||||||
inRoom: PropTypes.bool,
|
inRoom: PropTypes.bool,
|
||||||
onSettingsClick: PropTypes.func,
|
onSettingsClick: PropTypes.func,
|
||||||
onPinnedClick: PropTypes.func,
|
|
||||||
onSearchClick: PropTypes.func,
|
onSearchClick: PropTypes.func,
|
||||||
onLeaveClick: PropTypes.func,
|
onLeaveClick: PropTypes.func,
|
||||||
onCancelClick: PropTypes.func,
|
onCancelClick: PropTypes.func,
|
||||||
|
@ -59,14 +58,12 @@ export default class RoomHeader extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||||
cli.on("Room.accountData", this._onRoomAccountData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||||
cli.removeListener("Room.accountData", this._onRoomAccountData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,41 +76,14 @@ export default class RoomHeader extends React.Component {
|
||||||
this._rateLimitedUpdate();
|
this._rateLimitedUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomAccountData = (event, room) => {
|
|
||||||
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
|
|
||||||
if (event.getType() !== "im.vector.room.read_pins") return;
|
|
||||||
|
|
||||||
this._rateLimitedUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
||||||
/* eslint-disable babel/no-invalid-this */
|
/* eslint-disable babel/no-invalid-this */
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
_hasUnreadPins() {
|
|
||||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
|
||||||
if (!currentPinEvent) return false;
|
|
||||||
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
|
|
||||||
return false; // no pins == nothing to read
|
|
||||||
}
|
|
||||||
|
|
||||||
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
|
||||||
if (readPinsEvent && readPinsEvent.getContent()) {
|
|
||||||
const readStateEvents = readPinsEvent.getContent().event_ids || [];
|
|
||||||
if (readStateEvents) {
|
|
||||||
return !readStateEvents.includes(currentPinEvent.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's pins, and we haven't read any of them
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let searchStatus = null;
|
let searchStatus = null;
|
||||||
let cancelButton = null;
|
let cancelButton = null;
|
||||||
let pinnedEventsButton = null;
|
|
||||||
|
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||||
|
@ -174,22 +144,6 @@ export default class RoomHeader extends React.Component {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
|
|
||||||
let pinsIndicator = null;
|
|
||||||
if (this._hasUnreadPins()) {
|
|
||||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicatorUnread" />);
|
|
||||||
}
|
|
||||||
|
|
||||||
pinnedEventsButton =
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
|
|
||||||
onClick={this.props.onPinnedClick}
|
|
||||||
title={_t("Pinned Messages")}
|
|
||||||
>
|
|
||||||
{ pinsIndicator }
|
|
||||||
</AccessibleTooltipButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let forgetButton;
|
let forgetButton;
|
||||||
if (this.props.onForgetClick) {
|
if (this.props.onForgetClick) {
|
||||||
forgetButton =
|
forgetButton =
|
||||||
|
@ -239,7 +193,6 @@ export default class RoomHeader extends React.Component {
|
||||||
<div className="mx_RoomHeader_buttons">
|
<div className="mx_RoomHeader_buttons">
|
||||||
{ videoCallButton }
|
{ videoCallButton }
|
||||||
{ voiceCallButton }
|
{ voiceCallButton }
|
||||||
{ pinnedEventsButton }
|
|
||||||
{ forgetButton }
|
{ forgetButton }
|
||||||
{ appsButton }
|
{ appsButton }
|
||||||
{ searchButton }
|
{ searchButton }
|
||||||
|
@ -256,7 +209,7 @@ export default class RoomHeader extends React.Component {
|
||||||
{ topicElement }
|
{ topicElement }
|
||||||
{ cancelButton }
|
{ cancelButton }
|
||||||
{ rightRow }
|
{ rightRow }
|
||||||
<RoomHeaderButtons />
|
<RoomHeaderButtons room={this.props.room} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -601,10 +601,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: _td('Enable widget screenshots on supported widgets'),
|
displayName: _td('Enable widget screenshots on supported widgets'),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"PinnedEvents.isOpen": {
|
|
||||||
supportedLevels: [SettingLevel.ROOM_DEVICE],
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
"promptBeforeInviteUnknownUsers": {
|
"promptBeforeInviteUnknownUsers": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'),
|
displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'),
|
||||||
|
|
|
@ -24,6 +24,7 @@ export enum RightPanelPhases {
|
||||||
EncryptionPanel = 'EncryptionPanel',
|
EncryptionPanel = 'EncryptionPanel',
|
||||||
RoomSummary = 'RoomSummary',
|
RoomSummary = 'RoomSummary',
|
||||||
Widget = 'Widget',
|
Widget = 'Widget',
|
||||||
|
PinnedMessages = "PinnedMessages",
|
||||||
|
|
||||||
Room3pidMemberInfo = 'Room3pidMemberInfo',
|
Room3pidMemberInfo = 'Room3pidMemberInfo',
|
||||||
// Group stuff
|
// Group stuff
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue