feat: quick emoji reactions on hover, in-app notification toasts, mention pulse audit
CI / Build & Quality Checks (push) Failing after 6m6s
CI / Build & Quality Checks (push) Failing after 6m6s
P5-17: MessageQuickReactions moved from 3-dots menu to hover toolbar; shows 5 recent emoji directly on hover. Clicking a quick-reaction also closes any open emoji picker (setEmojiBoardAnchor). Line separator removed from component. P5-7: LotusToastContainer slides in from bottom-right when window is focused — replaces OS notification for in-focus events. Correct room path (DM vs home) derived from mDirectAtom. Invite toast routes to inbox. 4s auto-dismiss. Full TDS styling via CSS custom properties. P5-8: Confirmed already implemented upstream (MentionHighlightPulse, 0.6s scale+glow, one-shot, prefers-reduced-motion). Marked complete. Code-review fixes: toast navigation used nonexistent /room/ route; emoji picker stayed open after toolbar quick-reaction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { createRouter } from './Router';
|
||||
import { ScreenSizeProvider, useScreenSize } from '../hooks/useScreenSize';
|
||||
import { useCompositionEndTracking } from '../hooks/useComposingCheck';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { LotusToastContainer } from '../features/toast/LotusToastContainer';
|
||||
|
||||
function NightLightOverlay() {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
@@ -95,6 +96,7 @@ function App() {
|
||||
<JotaiProvider>
|
||||
<RouterProvider router={createRouter(clientConfig, screenSize)} />
|
||||
<NightLightOverlay />
|
||||
<LotusToastContainer />
|
||||
</JotaiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
|
||||
@@ -15,7 +15,13 @@ import { settingsAtom } from '../../state/settings';
|
||||
import { allInvitesAtom } from '../../state/room-list/inviteList';
|
||||
import { usePreviousValue } from '../../hooks/usePreviousValue';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getInboxInvitesPath, getInboxNotificationsPath } from '../pathUtils';
|
||||
import {
|
||||
getDirectRoomPath,
|
||||
getHomeRoomPath,
|
||||
getInboxInvitesPath,
|
||||
getInboxNotificationsPath,
|
||||
} from '../pathUtils';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import {
|
||||
getMemberDisplayName,
|
||||
getNotificationType,
|
||||
@@ -28,6 +34,7 @@ import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
|
||||
import { useInboxNotificationsSelected } from '../../hooks/router/useInbox';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { usePresenceUpdater } from '../../hooks/usePresenceUpdater';
|
||||
import { toastQueueAtom } from '../../state/toast';
|
||||
|
||||
function isInQuietHours(start: string, end: string): boolean {
|
||||
const now = new Date();
|
||||
@@ -107,6 +114,7 @@ function InviteNotifications() {
|
||||
const [quietHoursStart] = useSetting(settingsAtom, 'quietHoursStart');
|
||||
const [quietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd');
|
||||
const [inviteSoundId] = useSetting(settingsAtom, 'inviteSoundId');
|
||||
const setToast = useSetAtom(toastQueueAtom);
|
||||
|
||||
const soundSrc =
|
||||
inviteSoundId !== 'none' ? (NOTIFICATION_SOUND_MAP[inviteSoundId] ?? InviteSound) : null;
|
||||
@@ -123,6 +131,18 @@ function InviteNotifications() {
|
||||
|
||||
const notify = useCallback(
|
||||
(count: number) => {
|
||||
if (document.hasFocus()) {
|
||||
setToast({
|
||||
id: `invite-${Date.now()}`,
|
||||
displayName: 'Invitation',
|
||||
body: `You have ${count} new invitation request.`,
|
||||
roomName: 'Invites',
|
||||
roomId: '',
|
||||
hashPath: getInboxInvitesPath(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const noti = new window.Notification('Invitation', {
|
||||
icon: LogoSVG,
|
||||
badge: LogoSVG,
|
||||
@@ -135,7 +155,7 @@ function InviteNotifications() {
|
||||
noti.close();
|
||||
};
|
||||
},
|
||||
[navigate],
|
||||
[navigate, setToast],
|
||||
);
|
||||
|
||||
const playSound = useCallback(() => {
|
||||
@@ -194,6 +214,8 @@ function MessageNotifications() {
|
||||
const [quietHoursStart] = useSetting(settingsAtom, 'quietHoursStart');
|
||||
const [quietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd');
|
||||
const [messageSoundId] = useSetting(settingsAtom, 'messageSoundId');
|
||||
const setToast = useSetAtom(toastQueueAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
const soundSrc =
|
||||
messageSoundId !== 'none'
|
||||
@@ -219,13 +241,30 @@ function MessageNotifications() {
|
||||
roomName,
|
||||
roomAvatar,
|
||||
username,
|
||||
roomId,
|
||||
eventId,
|
||||
body,
|
||||
}: {
|
||||
roomName: string;
|
||||
roomAvatar?: string;
|
||||
username: string;
|
||||
roomId: string;
|
||||
eventId: string;
|
||||
body?: string;
|
||||
}) => {
|
||||
if (document.hasFocus()) {
|
||||
setToast({
|
||||
id: `${roomId}-${eventId}-${Date.now()}`,
|
||||
avatarUrl: roomAvatar,
|
||||
displayName: username,
|
||||
body: (body ?? '').slice(0, 80),
|
||||
roomName,
|
||||
roomId,
|
||||
hashPath: mDirects.has(roomId) ? getDirectRoomPath(roomId) : getHomeRoomPath(roomId),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const noti = new window.Notification(roomName, {
|
||||
icon: roomAvatar,
|
||||
badge: roomAvatar,
|
||||
@@ -242,7 +281,7 @@ function MessageNotifications() {
|
||||
notifRef.current?.close();
|
||||
notifRef.current = noti;
|
||||
},
|
||||
[navigate],
|
||||
[navigate, setToast, mDirects],
|
||||
);
|
||||
|
||||
const playSound = useCallback(() => {
|
||||
@@ -298,6 +337,7 @@ function MessageNotifications() {
|
||||
username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender,
|
||||
roomId: room.roomId,
|
||||
eventId,
|
||||
body: (mEvent.getContent().body as string | undefined) ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user