Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mark as unread #2

Merged
merged 18 commits into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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