Skip to content

Commit

Permalink
Merge pull request #2 from SchildiChat/mark-as-unread
Browse files Browse the repository at this point in the history
Mark as unread
  • Loading branch information
su-ex committed Nov 19, 2021
2 parents 23edea8 + 6c75c06 commit 2d49556
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 6 deletions.
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@
@import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomSublist.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTileSc.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SendMessageComposer.scss";
Expand Down
8 changes: 4 additions & 4 deletions res/css/views/rooms/_NotificationBadge.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ limitations under the License.
align-items: center;
justify-content: center;

&.mx_NotificationBadge_highlighted {
&.mx_NotificationBadge_highlighted, &.mx_NotificationBadge_highlighted.mx_NotificationBadge_dot {
// TODO: Use a more specific variable
background-color: $warning-color;
}
Expand All @@ -44,9 +44,9 @@ limitations under the License.
&.mx_NotificationBadge_dot {
background-color: $primary-content; // increased visibility

width: 6px;
height: 6px;
border-radius: 6px;
width: 8px;
height: 8px;
border-radius: 8px;
}

&.mx_NotificationBadge_2char {
Expand Down
8 changes: 8 additions & 0 deletions res/css/views/rooms/_RoomTileSc.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.mx_RoomTile_contextMenu {
.mx_RoomTile_markAsUnread::before {
mask-image: url('$(res)/img/element-icons/roomlist/mark-as-unread.svg');
}
.mx_RoomTile_markAsRead::before {
mask-image: url('$(res)/img/element-icons/roomlist/mark-as-read.svg');
}
}
1 change: 1 addition & 0 deletions res/img/element-icons/roomlist/mark-as-read.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions res/img/element-icons/roomlist/mark-as-unread.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions src/Rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";

import { MatrixClientPeg } from './MatrixClientPeg';
import AliasCustomisations from './customisations/Alias';
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";

/**
* Given a room object, return the alias we should use for it,
Expand Down Expand Up @@ -153,3 +154,19 @@ function guessDMRoomTargetId(room: Room, myUserId: string): string {
if (oldestUser === undefined) return myUserId;
return oldestUser.userId;
}

export const MARKED_UNREAD_TYPE = new UnstableValue("m.marked_unread", "com.famedly.marked_unread");

export function isRoomMarkedAsUnread(room: Room): boolean {
return room?.getAccountData(MARKED_UNREAD_TYPE.name)?.getContent()?.unread;
}

export async function setRoomMarkedAsUnread(room: Room, value = true): Promise<void> {
await MatrixClientPeg.get().setRoomAccountData(
room.roomId,
MARKED_UNREAD_TYPE.name,
{
unread: value,
},
);
}
17 changes: 17 additions & 0 deletions src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import { UserNameColorMode } from '../../settings/UserNameColorMode';

import { logger } from "matrix-js-sdk/src/logger";

import { isRoomMarkedAsUnread, setRoomMarkedAsUnread } from '../../Rooms';

const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
const READ_RECEIPT_INTERVAL_MS = 500;
Expand Down Expand Up @@ -870,6 +872,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
this.sendReadReceipt();
};

private removeUnreadMarker = () => {
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
// we still have a client.
const cli = MatrixClientPeg.get();
// if no client or client is guest don't send mark room as read
if (!cli || cli.isGuest()) return;

const markUnreadEnabled = SettingsStore.getValue("feature_mark_unread");
if (markUnreadEnabled && isRoomMarkedAsUnread(this.props.timelineSet.room)) {
setRoomMarkedAsUnread(this.props.timelineSet.room, false);
}
};

// advance the read marker past any events we sent ourselves.
private advanceReadMarkerPastMyEvents(): void {
if (!this.props.manageReadMarkers) return;
Expand Down Expand Up @@ -1121,6 +1137,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
if (this.props.sendReadReceiptOnLoad) {
this.sendReadReceipt();
}
this.removeUnreadMarker();
});
};

Expand Down
58 changes: 57 additions & 1 deletion src/components/views/rooms/RoomTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";

import { logger } from "matrix-js-sdk/src/logger";

import { isRoomMarkedAsUnread, setRoomMarkedAsUnread } from "../../../Rooms";
import SettingsStore from "../../../settings/SettingsStore";

interface IProps {
room: Room;
showMessagePreview: boolean;
Expand Down Expand Up @@ -149,6 +152,13 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
prevProps.room?.off("Room.name", this.onRoomNameUpdate);
this.props.room?.on("Room.name", this.onRoomNameUpdate);
}

// SC: Remove focus from room tile after hiding menu
if ((!!prevState.generalMenuPosition && !this.state.generalMenuPosition) ||
(!!prevState.notificationsMenuPosition && this.state.notificationsMenuPosition)) {
this.roomTileRef?.current?.focus();
this.roomTileRef?.current?.blur();
}
}

public componentDidMount() {
Expand Down Expand Up @@ -283,6 +293,36 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.setState({ generalMenuPosition: null });
};

private onMarkUnreadClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();

// Show home if current room is marked unread
if (this.state.selected) {
dis.dispatch({ action: 'view_home_page' });
}

setRoomMarkedAsUnread(this.props.room);
this.setState({ generalMenuPosition: null }); // hide the menu
};

private onMarkReadClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// Clear manually marked as unread
const markUnreadEnabled = SettingsStore.getValue("feature_mark_unread");
if (markUnreadEnabled) {
setRoomMarkedAsUnread(this.props.room, false);
}
// Update read receipt
const events = this.props.room.getLiveTimeline().getEvents();
if (events.length) {
// noinspection JSIgnoredPromiseFromCall
MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]);
}
this.setState({ generalMenuPosition: null }); // hide the menu
};

private onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
ev.preventDefault();
ev.stopPropagation();
Expand Down Expand Up @@ -440,7 +480,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {

// Only show the icon by default if the room is overridden to muted.
// TODO: [FTUE Notifications] Probably need to detect global mute state
mx_RoomTile_notificationsButton_show: state === RoomNotifState.Mute,
//mx_RoomTile_notificationsButton_show: state === RoomNotifState.Mute,
// SchildiChat: never show the icon by default. This gets in the way of the "marked as unread" icon.
mx_RoomTile_notificationsButton_show: false,
});

return (
Expand Down Expand Up @@ -487,13 +529,27 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {

const userId = MatrixClientPeg.get().getUserId();
const canInvite = this.props.room.canInvite(userId);
const markUnreadEnabled = SettingsStore.getValue("feature_mark_unread");
const isUnread = this.notificationState.isUnread ||
(markUnreadEnabled && isRoomMarkedAsUnread(this.props.room));

contextMenu = <IconizedContextMenu
{...contextMenuBelow(this.state.generalMenuPosition)}
onFinished={this.onCloseGeneralMenu}
className="mx_RoomTile_contextMenu"
compact
>
<IconizedContextMenuOptionList>
{ (markUnreadEnabled && !isUnread) ? (<IconizedContextMenuOption
onClick={this.onMarkUnreadClick}
label={_t("Mark as unread")}
iconClassName="mx_RoomTile_markAsUnread"
/>) : null }
{ (markUnreadEnabled && isUnread) ? (<IconizedContextMenuOption
onClick={this.onMarkReadClick}
label={_t("Mark as read")}
iconClassName="mx_RoomTile_markAsRead"
/>) : null }
<IconizedContextMenuCheckbox
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
active={isFavorite}
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Mark rooms as unread": "Mark rooms as unread",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
"Message Pinning": "Message Pinning",
"Threaded messaging": "Threaded messaging",
Expand Down Expand Up @@ -1780,6 +1781,7 @@
"Favourited": "Favourited",
"Favourite": "Favourite",
"Low Priority": "Low Priority",
"Mark as unread": "Mark as unread",
"Invite People": "Invite People",
"Copy Room Link": "Copy Room Link",
"Settings": "Settings",
Expand Down
6 changes: 6 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_mark_unread": {
isFeature: true,
displayName: _td("Mark rooms as unread"),
supportedLevels: LEVELS_FEATURE,
default: true,
},
"feature_communities_v2_prototypes": {
isFeature: true,
displayName: _td(
Expand Down
29 changes: 28 additions & 1 deletion src/stores/notifications/RoomNotificationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,26 @@ import * as Unread from '../../Unread';
import { NotificationState } from "./NotificationState";
import { getUnsentMessages } from "../../components/structures/RoomStatusBar";

import { isRoomMarkedAsUnread, MARKED_UNREAD_TYPE } from "../../Rooms";
import SettingsStore from "../../settings/SettingsStore";

export class RoomNotificationState extends NotificationState implements IDestroyable {
private featureMarkedUnreadWatcherRef = null;
constructor(public readonly room: Room) {
super();
this.room.on("Room.receipt", this.handleReadReceipt);
this.room.on("Room.timeline", this.handleRoomEventUpdate);
this.room.on("Room.redaction", this.handleRoomEventUpdate);
this.room.on("Room.myMembership", this.handleMembershipUpdate);
this.room.on("Room.localEchoUpdated", this.handleLocalEchoUpdated);
this.room.on("Room.accountData", this.handleRoomAccountDataUpdate);
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("accountData", this.handleAccountDataUpdate);

this.featureMarkedUnreadWatcherRef = SettingsStore.watchSetting("feature_mark_unread", null, () => {
this.updateNotificationState();
});

this.updateNotificationState();
}

Expand All @@ -50,10 +60,12 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
this.room.removeListener("Room.myMembership", this.handleMembershipUpdate);
this.room.removeListener("Room.localEchoUpdated", this.handleLocalEchoUpdated);
this.room.removeListener("Room.accountData", this.handleRoomAccountDataUpdate);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
MatrixClientPeg.get().removeListener("accountData", this.handleAccountDataUpdate);
}
SettingsStore.unwatchSetting(this.featureMarkedUnreadWatcherRef);
}

private handleLocalEchoUpdated = () => {
Expand Down Expand Up @@ -83,15 +95,25 @@ export class RoomNotificationState extends NotificationState implements IDestroy
}
};

private handleRoomAccountDataUpdate = (ev: MatrixEvent) => {
if (ev.getType() === MARKED_UNREAD_TYPE.name) {
this.updateNotificationState();
}
};

private updateNotificationState() {
const snapshot = this.snapshot();

const markUnreadEnabled = SettingsStore.getValue("feature_mark_unread");
const markedUnread = markUnreadEnabled && isRoomMarkedAsUnread(this.room);

if (getUnsentMessages(this.room).length > 0) {
// When there are unsent messages we show a red `!`
this._color = NotificationColor.Unsent;
this._symbol = "!";
this._count = 1; // not used, technically
} else if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.RoomNotifState.Mute) {
} else if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.RoomNotifState.Mute &&
!markedUnread) {
// When muted we suppress all notification states, even if we have context on them.
this._color = NotificationColor.None;
this._symbol = null;
Expand Down Expand Up @@ -121,10 +143,15 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this._color = NotificationColor.Grey;
this._count = trueCount;
this._symbol = null; // symbol calculated by component
} else if (markedUnread) {
this._color = NotificationColor.Grey;
this._symbol = "!";
this._count = 1; // not used, technically
} else {
// We don't have any notified messages, but we might have unread messages. Let's
// find out.
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.room);

if (hasUnread) {
this._color = NotificationColor.Bold;
} else {
Expand Down

0 comments on commit 2d49556

Please sign in to comment.