diff --git a/public/res/ic/outlined/message-unread.svg b/public/res/ic/outlined/message-unread.svg new file mode 100644 index 0000000..fc5e9ff --- /dev/null +++ b/public/res/ic/outlined/message-unread.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/public/res/ic/outlined/message.svg b/public/res/ic/outlined/message.svg new file mode 100644 index 0000000..d36e9a3 --- /dev/null +++ b/public/res/ic/outlined/message.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/app/molecules/room-options/RoomOptions.jsx b/src/app/molecules/room-options/RoomOptions.jsx index 734f93b..85b2b5a 100644 --- a/src/app/molecules/room-options/RoomOptions.jsx +++ b/src/app/molecules/room-options/RoomOptions.jsx @@ -6,6 +6,7 @@ import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import { openInviteUser } from '../../../client/action/navigation'; import * as roomActions from '../../../client/action/room'; +import { markAsRead } from '../../../client/action/notifications'; import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu'; import RoomNotification from '../room-notification/RoomNotification'; @@ -20,10 +21,8 @@ function RoomOptions({ roomId, afterOptionSelect }) { const canInvite = room?.canInvite(mx.getUserId()); const handleMarkAsRead = () => { + markAsRead(roomId); afterOptionSelect(); - if (!room) return; - const events = room.getLiveTimeline().getEvents(); - mx.sendReadReceipt(events[events.length - 1]); }; const handleInviteClick = () => { diff --git a/src/app/organisms/room/Room.jsx b/src/app/organisms/room/Room.jsx index 015fb68..7c199be 100644 --- a/src/app/organisms/room/Room.jsx +++ b/src/app/organisms/room/Room.jsx @@ -26,7 +26,7 @@ function Room() { roomInfo.roomTimeline?.removeInternalListeners(); if (mx.getRoom(rId)) { setRoomInfo({ - roomTimeline: new RoomTimeline(rId, initMatrix.notifications), + roomTimeline: new RoomTimeline(rId), eventId: eId ?? null, }); } else { diff --git a/src/app/organisms/room/RoomViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx index e3f2aba..dd77e98 100644 --- a/src/app/organisms/room/RoomViewContent.jsx +++ b/src/app/organisms/room/RoomViewContent.jsx @@ -14,6 +14,7 @@ import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import { openProfileViewer } from '../../../client/action/navigation'; import { diffMinutes, isInSameDay, Throttle } from '../../../util/common'; +import { markAsRead } from '../../../client/action/notifications'; import Divider from '../../atoms/divider/Divider'; import ScrollView from '../../atoms/scroll/ScrollView'; @@ -253,7 +254,7 @@ function useHandleScroll( ); roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom); if (isAtBottom && readUptoEvtStore.getItem()) { - requestAnimationFrame(() => roomTimeline.markAllAsRead()); + requestAnimationFrame(() => markAsRead(roomTimeline.roomId)); } }); autoPaginate(); @@ -263,7 +264,7 @@ function useHandleScroll( const timelineScroll = timelineScrollRef.current; const limit = eventLimitRef.current; if (readUptoEvtStore.getItem()) { - requestAnimationFrame(() => roomTimeline.markAllAsRead()); + requestAnimationFrame(() => markAsRead(roomTimeline.roomId)); } if (roomTimeline.isServingLiveTimeline()) { limit.setFrom(roomTimeline.timeline.length - limit.maxEvents); @@ -286,7 +287,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event const limit = eventLimitRef.current; const trySendReadReceipt = (event) => { if (myUserId === event.getSender()) { - requestAnimationFrame(() => roomTimeline.markAllAsRead()); + requestAnimationFrame(() => markAsRead(roomTimeline.roomId)); return; } const readUpToEvent = readUptoEvtStore.getItem(); @@ -295,7 +296,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event if (isUnread === false) { if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) { - requestAnimationFrame(() => roomTimeline.markAllAsRead()); + requestAnimationFrame(() => markAsRead(roomTimeline.roomId)); } else { readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId)); } @@ -305,7 +306,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event const { timeline } = roomTimeline; const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId; if (unreadMsgIsLast) { - requestAnimationFrame(() => roomTimeline.markAllAsRead()); + requestAnimationFrame(() => markAsRead(roomTimeline.roomId)); } }; @@ -399,7 +400,7 @@ function RoomViewContent({ eventId, roomTimeline }) { if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) { const readUpToId = roomTimeline.getReadUpToEventId(); if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) { - requestAnimationFrame(() => roomTimeline.markAllAsRead()); + requestAnimationFrame(() => markAsRead(roomTimeline.roomId)); } } jumpToItemIndex = -1; diff --git a/src/app/organisms/room/RoomViewFloating.jsx b/src/app/organisms/room/RoomViewFloating.jsx index 1bec52d..e65e854 100644 --- a/src/app/organisms/room/RoomViewFloating.jsx +++ b/src/app/organisms/room/RoomViewFloating.jsx @@ -5,11 +5,13 @@ import './RoomViewFloating.scss'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; +import { markAsRead } from '../../../client/action/notifications'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; import IconButton from '../../atoms/button/IconButton'; +import MessageUnreadIC from '../../../../public/res/ic/outlined/message-unread.svg'; import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; @@ -23,7 +25,7 @@ function useJumpToEvent(roomTimeline) { }; const cancelJumpToEvent = () => { - roomTimeline.markAllAsRead(); + markAsRead(roomTimeline.roomId); setEventId(null); }; @@ -36,11 +38,12 @@ function useJumpToEvent(roomTimeline) { setEventId(readEventId); } + const { notifications } = initMatrix; const handleMarkAsRead = () => setEventId(null); - roomTimeline.on(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead); + notifications.on(cons.events.notifications.FULL_READ, handleMarkAsRead); return () => { - roomTimeline.removeListener(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead); + notifications.removeListener(cons.events.notifications.FULL_READ, handleMarkAsRead); setEventId(null); }; }, [roomTimeline]); @@ -96,17 +99,12 @@ function RoomViewFloating({ return ( <>
- + -
0 ? ' room-view__typing--open' : ''}`}>
diff --git a/src/app/organisms/room/RoomViewFloating.scss b/src/app/organisms/room/RoomViewFloating.scss index 13de15c..263025e 100644 --- a/src/app/organisms/room/RoomViewFloating.scss +++ b/src/app/organisms/room/RoomViewFloating.scss @@ -90,33 +90,33 @@ &__unread { position: absolute; - top: var(--sp-extra-tight); - @include dir.prop(right, var(--sp-extra-tight), unset); - @include dir.prop(left, unset, var(--sp-extra-tight)); + top: 0; + @include dir.prop(left, var(--sp-normal), unset); + @include dir.prop(right, unset, var(--sp-normal)); z-index: 999; display: none; + width: calc(100% - var(--sp-extra-loose)); background-color: var(--bg-surface); - border-radius: var(--bo-radius); + border-radius: 0 0 var(--bo-radius) var(--bo-radius); box-shadow: var(--bs-primary-border); overflow: hidden; &--open { display: flex; } - - & .ic-btn { - padding: 6px var(--sp-extra-tight); - border-radius: 0; - } & .btn-primary { - @extend .cp-fx__item-one; - @include dir.side(margin, 0, 1px); + justify-content: flex-start; border-radius: 0; - padding: 0 var(--sp-tight); - &:focus { - background-color: var(--bg-primary-hover); + padding: 2px var(--sp-tight); + & .ic-raw { + width: 16px; + height: 16px; } } + & .btn-primary:first-child { + @extend .cp-fx__item-one; + padding: var(--sp-ultra-tight) var(--sp-extra-tight); + } } } \ No newline at end of file diff --git a/src/client/action/notifications.js b/src/client/action/notifications.js new file mode 100644 index 0000000..a869632 --- /dev/null +++ b/src/client/action/notifications.js @@ -0,0 +1,26 @@ +import initMatrix from '../initMatrix'; + +// eslint-disable-next-line import/prefer-default-export +export async function markAsRead(roomId) { + const mx = initMatrix.matrixClient; + const room = mx.getRoom(roomId); + if (!room) return; + initMatrix.notifications.deleteNoti(roomId); + + const timeline = room.getLiveTimeline().getEvents(); + const readEventId = room.getEventReadUpTo(mx.getUserId()); + + const getLatestValidEvent = () => { + for (let i = timeline.length - 1; i >= 0; i -= 1) { + const latestEvent = timeline[i]; + if (latestEvent.getId() === readEventId) return null; + if (!latestEvent.isSending()) return latestEvent; + } + return null; + }; + if (timeline.length === 0) return; + const latestEvent = getLatestValidEvent(); + if (latestEvent === null) return; + + await mx.sendReadReceipt(latestEvent); +} diff --git a/src/client/event/hotkeys.js b/src/client/event/hotkeys.js index 41b6fc8..11f72be 100644 --- a/src/client/event/hotkeys.js +++ b/src/client/event/hotkeys.js @@ -1,5 +1,6 @@ import { openSearch, toggleRoomSettings } from '../action/navigation'; import navigation from '../state/navigation'; +import { markAsRead } from '../action/notifications'; function listenKeyboard(event) { // Ctrl/Cmd + @@ -18,11 +19,19 @@ function listenKeyboard(event) { return; } - // esc - close room settings panel - if (event.keyCode === 27 && navigation.isRoomSettings) { - toggleRoomSettings(); + // esc + if (event.keyCode === 27) { + if (navigation.isRoomSettings) { + toggleRoomSettings(); + return; + } + if (navigation.selectedRoomId) { + markAsRead(navigation.selectedRoomId); + return; + } } + // Don't allow these keys to type/focus message field if ((event.keyCode !== 8 && event.keyCode < 48) || (event.keyCode >= 91 && event.keyCode <= 93) || (event.keyCode >= 112 && event.keyCode <= 183)) { diff --git a/src/client/state/RoomTimeline.js b/src/client/state/RoomTimeline.js index bccb197..57d91c1 100644 --- a/src/client/state/RoomTimeline.js +++ b/src/client/state/RoomTimeline.js @@ -77,7 +77,7 @@ function isTimelineLinked(tm1, tm2) { } class RoomTimeline extends EventEmitter { - constructor(roomId, notifications) { + constructor(roomId) { super(); // These are local timelines this.timeline = []; @@ -88,7 +88,6 @@ class RoomTimeline extends EventEmitter { this.matrixClient = initMatrix.matrixClient; this.roomId = roomId; this.room = this.matrixClient.getRoom(roomId); - this.notifications = notifications; this.liveTimeline = this.room.getLiveTimeline(); this.activeTimeline = this.liveTimeline; @@ -228,25 +227,6 @@ class RoomTimeline extends EventEmitter { return Promise.allSettled(decryptionPromises); } - markAllAsRead() { - const readEventId = this.getReadUpToEventId(); - const getLatestValidEvent = () => { - for (let i = this.timeline.length - 1; i >= 0; i -= 1) { - const latestEvent = this.timeline[i]; - if (latestEvent.getId() === readEventId) return null; - if (!latestEvent.isSending()) return latestEvent; - } - return null; - }; - this.notifications.deleteNoti(this.roomId); - if (this.timeline.length === 0) return; - const latestEvent = getLatestValidEvent(); - if (latestEvent === null) return; - if (readEventId === latestEvent.getId()) return; - this.matrixClient.sendReadReceipt(latestEvent); - this.emit(cons.events.roomTimeline.MARKED_AS_READ, latestEvent); - } - hasEventInTimeline(eventId, timeline = this.activeTimeline) { const timelineSet = this.getUnfilteredTimelineSet(); const eventTimeline = timelineSet.getTimelineForEvent(eventId); diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 862bf5c..f7ffc8f 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -116,7 +116,6 @@ const cons = { PAGINATED: 'PAGINATED', TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED', LIVE_RECEIPT: 'LIVE_RECEIPT', - MARKED_AS_READ: 'MARKED_AS_READ', EVENT_REDACTED: 'EVENT_REDACTED', AT_BOTTOM: 'AT_BOTTOM', SCROLL_TO_LIVE: 'SCROLL_TO_LIVE',