feat: implement useMarkAsRead hook for improved unread state management and update notification handling
This commit is contained in:
@@ -1,6 +1,14 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
import { color, config, toRem } from 'folds';
|
import { color, config, toRem } from 'folds';
|
||||||
|
|
||||||
|
export const CheckButtonContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '32px',
|
||||||
|
width: '32px',
|
||||||
|
});
|
||||||
|
|
||||||
export const CheckButton = style({
|
export const CheckButton = style({
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -11,13 +19,19 @@ export const CheckButton = style({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: color.Secondary.Main,
|
color: color.Secondary.Main,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
transition: 'background-color 0.15s',
|
transition: 'opacity 0.2s, background-color 0.15s',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
width: '32px',
|
width: '32px',
|
||||||
opacity: 0.7,
|
opacity: 0,
|
||||||
WebkitAppRegion: 'no-drag',
|
WebkitAppRegion: 'no-drag',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&[data-visible="true"]': {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
':hover': {
|
':hover': {
|
||||||
backgroundColor: color.Surface.ContainerHover,
|
backgroundColor: color.Surface.ContainerHover,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
@@ -30,6 +44,7 @@ export const CheckButton = style({
|
|||||||
':focus-visible': {
|
':focus-visible': {
|
||||||
outline: `2px solid ${color.Secondary.Main}`,
|
outline: `2px solid ${color.Secondary.Main}`,
|
||||||
outlineOffset: '2px',
|
outlineOffset: '2px',
|
||||||
|
opacity: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function UpdateNotification() {
|
|||||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||||
const [updateReady, setUpdateReady] = useState(false);
|
const [updateReady, setUpdateReady] = useState(false);
|
||||||
const [checking, setChecking] = useState(false);
|
const [checking, setChecking] = useState(false);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number; width: number; height: number } | undefined>(undefined);
|
const [menuAnchor, setMenuAnchor] = useState<{ x: number; y: number; width: number; height: number } | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -118,29 +119,36 @@ export function UpdateNotification() {
|
|||||||
// Show check button if no update status
|
// Show check button if no update status
|
||||||
if (!updateAvailable && !updateReady && !checking) {
|
if (!updateAvailable && !updateReady && !checking) {
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
className={css.CheckButton}
|
className={css.CheckButtonContainer}
|
||||||
onClick={handleCheckForUpdates}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
aria-label="Check for updates"
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
title="Check for updates"
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<button
|
||||||
<path
|
className={css.CheckButton}
|
||||||
d="M8 2V10M8 10L5 7M8 10L11 7"
|
data-visible={isHovered}
|
||||||
stroke="currentColor"
|
onClick={handleCheckForUpdates}
|
||||||
strokeWidth="1.5"
|
aria-label="Check for updates"
|
||||||
strokeLinecap="round"
|
title="Check for updates"
|
||||||
strokeLinejoin="round"
|
type="button"
|
||||||
/>
|
>
|
||||||
<path
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
d="M3 14H13"
|
<path
|
||||||
stroke="currentColor"
|
d="M8 2V10M8 10L5 7M8 10L11 7"
|
||||||
strokeWidth="1.5"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeWidth="1.5"
|
||||||
/>
|
strokeLinecap="round"
|
||||||
</svg>
|
strokeLinejoin="round"
|
||||||
</button>
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 14H13"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { useRoomUnread } from '../../state/hooks/unread';
|
|||||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
||||||
import { usePowerLevels } from '../../hooks/usePowerLevels';
|
import { usePowerLevels } from '../../hooks/usePowerLevels';
|
||||||
import { copyToClipboard } from '../../utils/dom';
|
import { copyToClipboard } from '../../utils/dom';
|
||||||
import { markAsRead } from '../../utils/notifications';
|
import { useMarkAsRead } from '../../hooks/useMarkAsRead';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||||
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
||||||
@@ -68,6 +68,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||||||
const powerLevels = usePowerLevels(room);
|
const powerLevels = usePowerLevels(room);
|
||||||
const creators = useRoomCreators(room);
|
const creators = useRoomCreators(room);
|
||||||
const { startCall, endCall, activeCall, callSupported } = useCall();
|
const { startCall, endCall, activeCall, callSupported } = useCall();
|
||||||
|
const markAsRead = useMarkAsRead(mx);
|
||||||
|
|
||||||
const permissions = useRoomPermissions(creators, powerLevels);
|
const permissions = useRoomPermissions(creators, powerLevels);
|
||||||
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
||||||
@@ -77,7 +78,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||||||
const [invitePrompt, setInvitePrompt] = useState(false);
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(room.roomId, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { settingsAtom } from '../../state/settings';
|
|||||||
import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels';
|
import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels';
|
||||||
import { useRoom } from '../../hooks/useRoom';
|
import { useRoom } from '../../hooks/useRoom';
|
||||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||||
import { markAsRead } from '../../utils/notifications';
|
import { useMarkAsRead } from '../../hooks/useMarkAsRead';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||||
import { activeRoomIdAtom } from '../../state/activeRoom';
|
import { activeRoomIdAtom } from '../../state/activeRoom';
|
||||||
@@ -27,6 +27,7 @@ export function Room() {
|
|||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
const powerLevels = usePowerLevels(room);
|
const powerLevels = usePowerLevels(room);
|
||||||
const members = useRoomMembers(mx, room.roomId);
|
const members = useRoomMembers(mx, room.roomId);
|
||||||
|
const markAsRead = useMarkAsRead(mx);
|
||||||
|
|
||||||
// Update titlebar with current room ID
|
// Update titlebar with current room ID
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,10 +40,10 @@ export function Room() {
|
|||||||
useCallback(
|
useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
if (isKeyHotkey('escape', evt)) {
|
if (isKeyHotkey('escape', evt)) {
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(room.roomId, hideActivity);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mx, room.roomId, hideActivity]
|
[markAsRead, room.roomId, hideActivity]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ import {
|
|||||||
getIntersectionObserverEntry,
|
getIntersectionObserverEntry,
|
||||||
useIntersectionObserver,
|
useIntersectionObserver,
|
||||||
} from '../../hooks/useIntersectionObserver';
|
} from '../../hooks/useIntersectionObserver';
|
||||||
import { markAsRead } from '../../utils/notifications';
|
import { useMarkAsRead } from '../../hooks/useMarkAsRead';
|
||||||
import { useDebounce } from '../../hooks/useDebounce';
|
import { useDebounce } from '../../hooks/useDebounce';
|
||||||
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
|
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
|
||||||
import * as css from './RoomTimeline.css';
|
import * as css from './RoomTimeline.css';
|
||||||
@@ -508,6 +508,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const spoilerClickHandler = useSpoilerClickHandler();
|
const spoilerClickHandler = useSpoilerClickHandler();
|
||||||
const openUserRoomProfile = useOpenUserRoomProfile();
|
const openUserRoomProfile = useOpenUserRoomProfile();
|
||||||
const space = useSpaceOptionally();
|
const space = useSpaceOptionally();
|
||||||
|
const markAsRead = useMarkAsRead(mx);
|
||||||
|
|
||||||
const imagePackRooms: Room[] = useImagePackRooms(room.roomId, roomToParents);
|
const imagePackRooms: Room[] = useImagePackRooms(room.roomId, roomToParents);
|
||||||
|
|
||||||
@@ -641,7 +642,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
// Check if the document is in focus (user is actively viewing the app),
|
// Check if the document is in focus (user is actively viewing the app),
|
||||||
// and either there are no unread messages or the latest message is from the current user.
|
// and either there are no unread messages or the latest message is from the current user.
|
||||||
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
||||||
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!, hideActivity));
|
requestAnimationFrame(() => markAsRead(mEvt.getRoomId()!, hideActivity));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.hasFocus() && !unreadInfo) {
|
if (!document.hasFocus() && !unreadInfo) {
|
||||||
@@ -665,7 +666,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
setUnreadInfo(getRoomUnreadInfo(room));
|
setUnreadInfo(getRoomUnreadInfo(room));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mx, room, unreadInfo, hideActivity]
|
[mx, room, unreadInfo, hideActivity, markAsRead]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -734,15 +735,15 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const tryAutoMarkAsRead = useCallback(() => {
|
const tryAutoMarkAsRead = useCallback(() => {
|
||||||
const readUptoEventId = readUptoEventIdRef.current;
|
const readUptoEventId = readUptoEventIdRef.current;
|
||||||
if (!readUptoEventId) {
|
if (!readUptoEventId) {
|
||||||
requestAnimationFrame(() => markAsRead(mx, room.roomId, hideActivity));
|
requestAnimationFrame(() => markAsRead(room.roomId, hideActivity));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const evtTimeline = getEventTimeline(room, readUptoEventId);
|
const evtTimeline = getEventTimeline(room, readUptoEventId);
|
||||||
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
||||||
if (latestTimeline === room.getLiveTimeline()) {
|
if (latestTimeline === room.getLiveTimeline()) {
|
||||||
requestAnimationFrame(() => markAsRead(mx, room.roomId, hideActivity));
|
requestAnimationFrame(() => markAsRead(room.roomId, hideActivity));
|
||||||
}
|
}
|
||||||
}, [mx, room, hideActivity]);
|
}, [room, hideActivity, markAsRead]);
|
||||||
|
|
||||||
const debounceSetAtBottom = useDebounce(
|
const debounceSetAtBottom = useDebounce(
|
||||||
useCallback((entry: IntersectionObserverEntry) => {
|
useCallback((entry: IntersectionObserverEntry) => {
|
||||||
@@ -946,7 +947,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(room.roomId, hideActivity);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenReply: MouseEventHandler = useCallback(
|
const handleOpenReply: MouseEventHandler = useCallback(
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ import { _SearchPathSearchParams } from '../../pages/paths';
|
|||||||
import * as css from './RoomViewHeader.css';
|
import * as css from './RoomViewHeader.css';
|
||||||
import { useRoomUnread } from '../../state/hooks/unread';
|
import { useRoomUnread } from '../../state/hooks/unread';
|
||||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||||
import { markAsRead } from '../../utils/notifications';
|
|
||||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
||||||
|
import { useMarkAsRead } from '../../hooks/useMarkAsRead';
|
||||||
import { copyToClipboard } from '../../utils/dom';
|
import { copyToClipboard } from '../../utils/dom';
|
||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
||||||
@@ -91,11 +91,12 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
|||||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||||
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
|
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
const markAsRead = useMarkAsRead(mx);
|
||||||
|
|
||||||
const [invitePrompt, setInvitePrompt] = useState(false);
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(room.roomId, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
51
src/app/hooks/useMarkAsRead.ts
Normal file
51
src/app/hooks/useMarkAsRead.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
import { markAsRead } from '../utils/notifications';
|
||||||
|
import { roomToUnreadAtom } from '../state/room/roomToUnread';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that marks a room as read and immediately updates local unread state.
|
||||||
|
* This fixes the issue where unread dots persist after refresh because the
|
||||||
|
* server hasn't processed the read receipt yet.
|
||||||
|
*
|
||||||
|
* @param mx - Matrix client instance
|
||||||
|
* @returns A function to mark rooms as read
|
||||||
|
*/
|
||||||
|
export function useMarkAsRead(mx: MatrixClient) {
|
||||||
|
const setUnreadAtom = useSetAtom(roomToUnreadAtom);
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async (roomId: string, privateReceipt: boolean) => {
|
||||||
|
// Immediately clear local unread state so UI updates instantly
|
||||||
|
setUnreadAtom({ type: 'DELETE', roomId });
|
||||||
|
|
||||||
|
// Then send the read receipt to the server
|
||||||
|
await markAsRead(mx, roomId, privateReceipt);
|
||||||
|
},
|
||||||
|
[mx, setUnreadAtom]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that marks multiple rooms as read simultaneously.
|
||||||
|
*
|
||||||
|
* @param mx - Matrix client instance
|
||||||
|
* @returns A function to mark multiple rooms as read
|
||||||
|
*/
|
||||||
|
export function useMarkRoomsAsRead(mx: MatrixClient) {
|
||||||
|
const setUnreadAtom = useSetAtom(roomToUnreadAtom);
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async (roomIds: string[], privateReceipt: boolean) => {
|
||||||
|
// Immediately clear local unread state for all rooms
|
||||||
|
roomIds.forEach((roomId) => {
|
||||||
|
setUnreadAtom({ type: 'DELETE', roomId });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send read receipts to the server
|
||||||
|
await Promise.all(roomIds.map((roomId) => markAsRead(mx, roomId, privateReceipt)));
|
||||||
|
},
|
||||||
|
[mx, setUnreadAtom]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ import { useDirectRooms } from './useDirectRooms';
|
|||||||
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
|
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
|
||||||
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
|
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
|
||||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||||
import { markAsRead } from '../../../utils/notifications';
|
import { useMarkRoomsAsRead } from '../../../hooks/useMarkAsRead';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
import { useSetting } from '../../../state/hooks/settings';
|
import { useSetting } from '../../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../../state/settings';
|
import { settingsAtom } from '../../../state/settings';
|
||||||
@@ -60,10 +60,11 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
|
|||||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||||
const orphanRooms = useDirectRooms();
|
const orphanRooms = useDirectRooms();
|
||||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||||
|
const markRoomsAsRead = useMarkRoomsAsRead(mx);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
if (!unread) return;
|
if (!unread) return;
|
||||||
orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
|
markRoomsAsRead(orphanRooms, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
|
|||||||
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
|
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
|
||||||
import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page';
|
import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page';
|
||||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||||
import { markAsRead } from '../../../utils/notifications';
|
import { useMarkRoomsAsRead } from '../../../hooks/useMarkAsRead';
|
||||||
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
|
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
import { useSetting } from '../../../state/hooks/settings';
|
import { useSetting } from '../../../state/hooks/settings';
|
||||||
@@ -78,10 +78,11 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(
|
|||||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const markRoomsAsRead = useMarkRoomsAsRead(mx);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
if (!unread) return;
|
if (!unread) return;
|
||||||
orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
|
markRoomsAsRead(orphanRooms, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ import * as customHtmlCss from '../../../styles/CustomHtml.css';
|
|||||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||||
import { useRoomUnread } from '../../../state/hooks/unread';
|
import { useRoomUnread } from '../../../state/hooks/unread';
|
||||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||||
import { markAsRead } from '../../../utils/notifications';
|
import { useMarkAsRead } from '../../../hooks/useMarkAsRead';
|
||||||
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
||||||
import { VirtualTile } from '../../../components/virtualizer';
|
import { VirtualTile } from '../../../components/virtualizer';
|
||||||
import { UserAvatar } from '../../../components/user-avatar';
|
import { UserAvatar } from '../../../components/user-avatar';
|
||||||
@@ -374,6 +374,7 @@ function RoomNotificationsGroupComp({
|
|||||||
|
|
||||||
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||||
const spoilerClickHandler = useSpoilerClickHandler();
|
const spoilerClickHandler = useSpoilerClickHandler();
|
||||||
|
const markAsRead = useMarkAsRead(mx);
|
||||||
|
|
||||||
const linkifyOpts = useMemo<LinkifyOpts>(
|
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -540,7 +541,7 @@ function RoomNotificationsGroupComp({
|
|||||||
onOpen(room.roomId, eventId);
|
onOpen(room.roomId, eventId);
|
||||||
};
|
};
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(room.roomId, hideActivity);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { Box, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text, config, toRem } from 'folds';
|
import { Box, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text, config, toRem } from 'folds';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import PaarrotSVG from '../../../../../public/res/svg/paarrot.svg';
|
||||||
|
import PaarrotUnreadSVG from '../../../../../public/res/svg/paarrot-unread.svg';
|
||||||
|
import PaarrotHighlightSVG from '../../../../../public/res/svg/paarrot-highlight.svg';
|
||||||
import { useDirects } from '../../../state/hooks/roomList';
|
import { useDirects } from '../../../state/hooks/roomList';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { mDirectAtom } from '../../../state/mDirectList';
|
import { mDirectAtom } from '../../../state/mDirectList';
|
||||||
@@ -21,7 +24,7 @@ import { UnreadBadge } from '../../../components/unread-badge';
|
|||||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||||
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
|
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
|
||||||
import { useDirectRooms } from '../direct/useDirectRooms';
|
import { useDirectRooms } from '../direct/useDirectRooms';
|
||||||
import { markAsRead } from '../../../utils/notifications';
|
import { useMarkRoomsAsRead } from '../../../hooks/useMarkAsRead';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
import { settingsAtom } from '../../../state/settings';
|
import { settingsAtom } from '../../../state/settings';
|
||||||
import { useSetting } from '../../../state/hooks/settings';
|
import { useSetting } from '../../../state/hooks/settings';
|
||||||
@@ -34,10 +37,11 @@ const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }
|
|||||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const markRoomsAsRead = useMarkRoomsAsRead(mx);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
if (!unread) return;
|
if (!unread) return;
|
||||||
orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
|
markRoomsAsRead(orphanRooms, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,11 +102,20 @@ export function DirectTab() {
|
|||||||
<SidebarAvatar
|
<SidebarAvatar
|
||||||
as="button"
|
as="button"
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
outlined
|
|
||||||
onClick={handleDirectClick}
|
onClick={handleDirectClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<Icon src={Icons.User} filled={directSelected} />
|
<img
|
||||||
|
src={
|
||||||
|
directUnread?.highlight
|
||||||
|
? PaarrotHighlightSVG
|
||||||
|
: directUnread?.total
|
||||||
|
? PaarrotUnreadSVG
|
||||||
|
: PaarrotSVG
|
||||||
|
}
|
||||||
|
alt="Direct Messages"
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
</SidebarAvatar>
|
</SidebarAvatar>
|
||||||
)}
|
)}
|
||||||
</SidebarItemTooltip>
|
</SidebarItemTooltip>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { UnreadBadge } from '../../../components/unread-badge';
|
|||||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||||
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
|
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
|
||||||
import { useHomeRooms } from '../home/useHomeRooms';
|
import { useHomeRooms } from '../home/useHomeRooms';
|
||||||
import { markAsRead } from '../../../utils/notifications';
|
import { useMarkRoomsAsRead } from '../../../hooks/useMarkAsRead';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
import { useSetting } from '../../../state/hooks/settings';
|
import { useSetting } from '../../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../../state/settings';
|
import { settingsAtom } from '../../../state/settings';
|
||||||
@@ -39,10 +39,11 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose, onHi
|
|||||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const markRoomsAsRead = useMarkRoomsAsRead(mx);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
if (!unread) return;
|
if (!unread) return;
|
||||||
orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
|
markRoomsAsRead(orphanRooms, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFo
|
|||||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||||
import { markAsRead } from '../../../utils/notifications';
|
import { useMarkRoomsAsRead } from '../../../hooks/useMarkAsRead';
|
||||||
import { copyToClipboard } from '../../../utils/dom';
|
import { copyToClipboard } from '../../../utils/dom';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
||||||
@@ -123,6 +123,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
|||||||
const permissions = useRoomPermissions(creators, powerLevels);
|
const permissions = useRoomPermissions(creators, powerLevels);
|
||||||
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
||||||
const openSpaceSettings = useOpenSpaceSettings();
|
const openSpaceSettings = useOpenSpaceSettings();
|
||||||
|
const markRoomsAsRead = useMarkRoomsAsRead(mx);
|
||||||
|
|
||||||
const [invitePrompt, setInvitePrompt] = useState(false);
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
|||||||
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
|
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
allChild.forEach((childRoomId) => markAsRead(mx, childRoomId, hideActivity));
|
markRoomsAsRead(allChild, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page
|
|||||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||||
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
|
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
|
||||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||||
import { markAsRead } from '../../../utils/notifications';
|
import { useMarkRoomsAsRead } from '../../../hooks/useMarkAsRead';
|
||||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||||
import { LeaveSpacePrompt } from '../../../components/leave-space-prompt';
|
import { LeaveSpacePrompt } from '../../../components/leave-space-prompt';
|
||||||
@@ -101,6 +101,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
|||||||
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
||||||
const openSpaceSettings = useOpenSpaceSettings();
|
const openSpaceSettings = useOpenSpaceSettings();
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
const markRoomsAsRead = useMarkRoomsAsRead(mx);
|
||||||
|
|
||||||
const [invitePrompt, setInvitePrompt] = useState(false);
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
|||||||
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
|
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
allChild.forEach((childRoomId) => markAsRead(mx, childRoomId, hideActivity));
|
markRoomsAsRead(allChild, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,29 +4,42 @@
|
|||||||
*/
|
*/
|
||||||
export const openExternalUrl = async (url: string): Promise<void> => {
|
export const openExternalUrl = async (url: string): Promise<void> => {
|
||||||
console.log('[openExternalUrl] called with:', url);
|
console.log('[openExternalUrl] called with:', url);
|
||||||
console.log('[openExternalUrl] __TAURI__ exists:', !!(window as any).__TAURI__);
|
|
||||||
|
|
||||||
// Only run this in Tauri builds
|
// Use Electron's shell.openExternal if in Electron
|
||||||
if (typeof window !== 'undefined' && (window as any).__TAURI__) {
|
if (isElectron()) {
|
||||||
|
try {
|
||||||
|
const electron = (window as any).electron;
|
||||||
|
if (electron?.shell?.openExternal) {
|
||||||
|
await electron.shell.openExternal(url);
|
||||||
|
console.log('[openExternalUrl] Electron shell.openExternal succeeded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[openExternalUrl] Electron shell.openExternal failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Tauri for actual Tauri builds
|
||||||
|
if (isTauri() && !isElectron()) {
|
||||||
try {
|
try {
|
||||||
// First, try the plugin directly (works on both desktop and mobile)
|
// First, try the plugin directly (works on both desktop and mobile)
|
||||||
console.log('[openExternalUrl] trying opener plugin...');
|
console.log('[openExternalUrl] trying Tauri opener plugin...');
|
||||||
const { openUrl } = await import('@tauri-apps/plugin-opener');
|
const { openUrl } = await import('@tauri-apps/plugin-opener');
|
||||||
await openUrl(url);
|
await openUrl(url);
|
||||||
console.log('[openExternalUrl] plugin succeeded');
|
console.log('[openExternalUrl] Tauri plugin succeeded');
|
||||||
return;
|
return;
|
||||||
} catch (pluginErr) {
|
} catch (pluginErr) {
|
||||||
console.warn('[openExternalUrl] opener plugin failed:', pluginErr);
|
console.warn('[openExternalUrl] Tauri opener plugin failed:', pluginErr);
|
||||||
|
|
||||||
// Fallback: try the custom command (useful if plugin fails due to ACL)
|
// Fallback: try the custom command (useful if plugin fails due to ACL)
|
||||||
try {
|
try {
|
||||||
console.log('[openExternalUrl] trying invoke command fallback...');
|
console.log('[openExternalUrl] trying Tauri invoke command fallback...');
|
||||||
const { invoke } = await import('@tauri-apps/api/core');
|
const { invoke } = await import('@tauri-apps/api/core');
|
||||||
await invoke('open_external_url', { url });
|
await invoke('open_external_url', { url });
|
||||||
console.log('[openExternalUrl] command fallback succeeded');
|
console.log('[openExternalUrl] Tauri command fallback succeeded');
|
||||||
return;
|
return;
|
||||||
} catch (invokeErr) {
|
} catch (invokeErr) {
|
||||||
console.error('[openExternalUrl] command fallback also failed:', invokeErr);
|
console.error('[openExternalUrl] Tauri command fallback also failed:', invokeErr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,15 +92,29 @@ let notificationTapCallback: ((path: string) => void) | null = null;
|
|||||||
* Bring the Tauri window to the front and focus it
|
* Bring the Tauri window to the front and focus it
|
||||||
*/
|
*/
|
||||||
export const focusWindow = async (): Promise<void> => {
|
export const focusWindow = async (): Promise<void> => {
|
||||||
if (!isTauri()) return;
|
// Use Electron's window focus if in Electron
|
||||||
|
if (isElectron()) {
|
||||||
|
try {
|
||||||
|
const electron = (window as any).electron;
|
||||||
|
if (electron?.window?.focus) {
|
||||||
|
await electron.window.focus();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to focus Electron window:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
// Use Tauri's window focus for actual Tauri builds
|
||||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
if (isTauri() && !isElectron()) {
|
||||||
const window = getCurrentWindow();
|
try {
|
||||||
await window.unminimize();
|
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||||
await window.setFocus();
|
const window = getCurrentWindow();
|
||||||
} catch (err) {
|
await window.unminimize();
|
||||||
console.warn('Failed to focus window:', err);
|
await window.setFocus();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to focus Tauri window:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,28 +123,43 @@ export const focusWindow = async (): Promise<void> => {
|
|||||||
* Call this once at app startup with a navigation callback
|
* Call this once at app startup with a navigation callback
|
||||||
*/
|
*/
|
||||||
export const setupNotificationTapListener = async (onTap: (path: string) => void): Promise<void> => {
|
export const setupNotificationTapListener = async (onTap: (path: string) => void): Promise<void> => {
|
||||||
if (!isTauri()) return;
|
|
||||||
|
|
||||||
notificationTapCallback = onTap;
|
notificationTapCallback = onTap;
|
||||||
|
|
||||||
try {
|
// Use Electron's native notification handler if in Electron
|
||||||
const { onAction } = await import('@tauri-apps/plugin-notification');
|
if (isElectron()) {
|
||||||
|
try {
|
||||||
await onAction(async (notification) => {
|
const electron = (window as any).electron;
|
||||||
// Bring window to front first
|
if (electron?.notification?.onNavigate) {
|
||||||
await focusWindow();
|
electron.notification.onNavigate((data: { path?: string }) => {
|
||||||
|
if (data.path && typeof data.path === 'string' && notificationTapCallback) {
|
||||||
// Get the path from extra data and navigate
|
notificationTapCallback(data.path);
|
||||||
const path = notification.notification?.extra?.path;
|
}
|
||||||
if (path && typeof path === 'string' && notificationTapCallback) {
|
});
|
||||||
notificationTapCallback(path);
|
return;
|
||||||
}
|
}
|
||||||
});
|
} catch (err) {
|
||||||
} catch (err) {
|
console.warn('Failed to set up Electron notification listener:', err);
|
||||||
// Silently ignore errors in Electron compatibility layer
|
return;
|
||||||
// Only warn if we're actually in a Tauri environment
|
}
|
||||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
}
|
||||||
console.warn('Failed to set up notification tap listener:', err);
|
|
||||||
|
// Use Tauri plugin for actual Tauri builds (not Electron)
|
||||||
|
if (isTauri() && !isElectron()) {
|
||||||
|
try {
|
||||||
|
const { onAction } = await import('@tauri-apps/plugin-notification');
|
||||||
|
|
||||||
|
await onAction(async (notification) => {
|
||||||
|
// Bring window to front first
|
||||||
|
await focusWindow();
|
||||||
|
|
||||||
|
// Get the path from extra data and navigate
|
||||||
|
const path = notification.notification?.extra?.path;
|
||||||
|
if (path && typeof path === 'string' && notificationTapCallback) {
|
||||||
|
notificationTapCallback(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to set up Tauri notification tap listener:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -128,11 +170,17 @@ export const setupNotificationTapListener = async (onTap: (path: string) => void
|
|||||||
export const isTauri = (): boolean =>
|
export const isTauri = (): boolean =>
|
||||||
'__TAURI__' in window || '__TAURI_INTERNALS__' in window;
|
'__TAURI__' in window || '__TAURI_INTERNALS__' in window;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're running inside Electron (not Tauri)
|
||||||
|
*/
|
||||||
|
export const isElectron = (): boolean =>
|
||||||
|
typeof window !== 'undefined' && 'electron' in window;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we're running on a mobile platform (Android/iOS)
|
* Check if we're running on a mobile platform (Android/iOS)
|
||||||
*/
|
*/
|
||||||
export const isTauriMobile = (): boolean => {
|
export const isTauriMobile = (): boolean => {
|
||||||
if (!isTauri()) return false;
|
if (!isTauri() || isElectron()) return false;
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
return ua.includes('android') || ua.includes('iphone') || ua.includes('ipad');
|
return ua.includes('android') || ua.includes('iphone') || ua.includes('ipad');
|
||||||
};
|
};
|
||||||
@@ -242,7 +290,26 @@ export const sendNotification = async (options: {
|
|||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const { title, body, icon, path, onClick } = options;
|
const { title, body, icon, path, onClick } = options;
|
||||||
|
|
||||||
if (isTauri()) {
|
// Use Electron's native notification API if in Electron
|
||||||
|
if (isElectron()) {
|
||||||
|
try {
|
||||||
|
const electron = (window as any).electron;
|
||||||
|
if (electron?.notification?.show) {
|
||||||
|
await electron.notification.show({
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
icon,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Electron notification failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Tauri plugin for actual Tauri builds (not Electron)
|
||||||
|
if (isTauri() && !isElectron()) {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
sendNotification: tauriSendNotification,
|
sendNotification: tauriSendNotification,
|
||||||
@@ -269,14 +336,14 @@ export const sendNotification = async (options: {
|
|||||||
extra: path ? { path } : undefined,
|
extra: path ? { path } : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Tauri notification failed:', err);
|
console.warn('Tauri notification failed:', err);
|
||||||
// Fallback to browser notification on error
|
|
||||||
sendBrowserNotification({ title, body, icon, onClick });
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
sendBrowserNotification({ title, body, icon, onClick });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to browser notification
|
||||||
|
sendBrowserNotification({ title, body, icon, onClick });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -292,22 +359,44 @@ export const isLinux = (): boolean => {
|
|||||||
* Returns a data URL if an image is found, null otherwise
|
* Returns a data URL if an image is found, null otherwise
|
||||||
*/
|
*/
|
||||||
export const readClipboardImage = async (): Promise<File | null> => {
|
export const readClipboardImage = async (): Promise<File | null> => {
|
||||||
if (!isTauri() || !isLinux()) return null;
|
// Use Electron's clipboard API if in Electron
|
||||||
|
if (isElectron()) {
|
||||||
try {
|
try {
|
||||||
const { invoke } = await import('@tauri-apps/api/core');
|
const electron = (window as any).electron;
|
||||||
const dataUrl = await invoke<string | null>('read_clipboard_image');
|
if (electron?.clipboard?.readImage) {
|
||||||
|
const dataUrl = await electron.clipboard.readImage();
|
||||||
if (!dataUrl) return null;
|
if (!dataUrl) return null;
|
||||||
|
|
||||||
// Convert data URL to File
|
// Convert data URL to File
|
||||||
const response = await fetch(dataUrl);
|
const response = await fetch(dataUrl);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
return new File([blob], 'clipboard-image.png', { type: 'image/png' });
|
return new File([blob], 'clipboard-image.png', { type: 'image/png' });
|
||||||
} catch (err) {
|
}
|
||||||
console.warn('Failed to read clipboard image:', err);
|
} catch (err) {
|
||||||
return null;
|
console.warn('Failed to read Electron clipboard image:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use Tauri's invoke for actual Tauri builds on Linux
|
||||||
|
if (isTauri() && !isElectron() && isLinux()) {
|
||||||
|
try {
|
||||||
|
const { invoke } = await import('@tauri-apps/api/core');
|
||||||
|
const dataUrl = await invoke<string | null>('read_clipboard_image');
|
||||||
|
|
||||||
|
if (!dataUrl) return null;
|
||||||
|
|
||||||
|
// Convert data URL to File
|
||||||
|
const response = await fetch(dataUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
return new File([blob], 'clipboard-image.png', { type: 'image/png' });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to read Tauri clipboard image:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ async function checkForUpdates() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only run in Tauri context
|
// Only run in Tauri context (not Electron)
|
||||||
if (!isTauri()) {
|
if (!isTauri() || isElectron()) {
|
||||||
console.log('Update check skipped - not running in Tauri');
|
console.log('Update check skipped - not running in Tauri');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user