feat: enhance Tauri support with notification handling and safe area insets
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover" />
|
||||||
<title>Cinny</title>
|
<title>Cinny</title>
|
||||||
<meta name="name" content="Cinny" />
|
<meta name="name" content="Cinny" />
|
||||||
<meta name="author" content="Ajay Bura" />
|
<meta name="author" content="Ajay Bura" />
|
||||||
|
|||||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -16,6 +16,9 @@
|
|||||||
"@tanstack/react-query": "5.24.1",
|
"@tanstack/react-query": "5.24.1",
|
||||||
"@tanstack/react-query-devtools": "5.24.1",
|
"@tanstack/react-query-devtools": "5.24.1",
|
||||||
"@tanstack/react-virtual": "3.2.0",
|
"@tanstack/react-virtual": "3.2.0",
|
||||||
|
"@tauri-apps/api": "2.9.1",
|
||||||
|
"@tauri-apps/plugin-fs": "2.4.5",
|
||||||
|
"@tauri-apps/plugin-notification": "2.3.3",
|
||||||
"@vanilla-extract/css": "1.9.3",
|
"@vanilla-extract/css": "1.9.3",
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
"@vanilla-extract/vite-plugin": "3.7.1",
|
"@vanilla-extract/vite-plugin": "3.7.1",
|
||||||
@@ -4523,6 +4526,34 @@
|
|||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/api": {
|
||||||
|
"version": "2.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz",
|
||||||
|
"integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==",
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-fs": {
|
||||||
|
"version": "2.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz",
|
||||||
|
"integrity": "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-notification": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
"@tanstack/react-query": "5.24.1",
|
"@tanstack/react-query": "5.24.1",
|
||||||
"@tanstack/react-query-devtools": "5.24.1",
|
"@tanstack/react-query-devtools": "5.24.1",
|
||||||
"@tanstack/react-virtual": "3.2.0",
|
"@tanstack/react-virtual": "3.2.0",
|
||||||
|
"@tauri-apps/api": "2.9.1",
|
||||||
|
"@tauri-apps/plugin-fs": "2.4.5",
|
||||||
|
"@tauri-apps/plugin-notification": "2.3.3",
|
||||||
"@vanilla-extract/css": "1.9.3",
|
"@vanilla-extract/css": "1.9.3",
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
"@vanilla-extract/vite-plugin": "3.7.1",
|
"@vanilla-extract/vite-plugin": "3.7.1",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export type Theme = {
|
|||||||
export const LightTheme: Theme = {
|
export const LightTheme: Theme = {
|
||||||
id: 'light-theme',
|
id: 'light-theme',
|
||||||
kind: ThemeKind.Light,
|
kind: ThemeKind.Light,
|
||||||
classNames: [lightTheme, onLightFontWeight, 'prism-light'],
|
classNames: ['light-theme', lightTheme, onLightFontWeight, 'prism-light'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SilverTheme: Theme = {
|
export const SilverTheme: Theme = {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { settingsAtom } from '../../state/settings';
|
|||||||
import { allInvitesAtom } from '../../state/room-list/inviteList';
|
import { allInvitesAtom } from '../../state/room-list/inviteList';
|
||||||
import { usePreviousValue } from '../../hooks/usePreviousValue';
|
import { usePreviousValue } from '../../hooks/usePreviousValue';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { getInboxInvitesPath, getInboxNotificationsPath } from '../pathUtils';
|
import { getDirectRoomPath, getHomeRoomPath, getInboxInvitesPath } from '../pathUtils';
|
||||||
import {
|
import {
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getNotificationType,
|
getNotificationType,
|
||||||
@@ -26,6 +26,7 @@ import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
|||||||
import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
|
||||||
import { useInboxNotificationsSelected } from '../../hooks/router/useInbox';
|
import { useInboxNotificationsSelected } from '../../hooks/router/useInbox';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
import { isTauri, sendNotification, setupNotificationTapListener } from '../../utils/tauri';
|
||||||
|
|
||||||
function SystemEmojiFeature() {
|
function SystemEmojiFeature() {
|
||||||
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||||
@@ -88,17 +89,29 @@ function InviteNotifications() {
|
|||||||
|
|
||||||
const notify = useCallback(
|
const notify = useCallback(
|
||||||
(count: number) => {
|
(count: number) => {
|
||||||
const noti = new window.Notification('Invitation', {
|
const body = `You have ${count} new invitation request.`;
|
||||||
icon: LogoSVG,
|
|
||||||
badge: LogoSVG,
|
|
||||||
body: `You have ${count} new invitation request.`,
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
noti.onclick = () => {
|
if (isTauri()) {
|
||||||
if (!window.closed) navigate(getInboxInvitesPath());
|
sendNotification({
|
||||||
noti.close();
|
title: 'Invitation',
|
||||||
};
|
body,
|
||||||
|
onClick: () => {
|
||||||
|
if (!window.closed) navigate(getInboxInvitesPath());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const noti = new window.Notification('Invitation', {
|
||||||
|
icon: LogoSVG,
|
||||||
|
badge: LogoSVG,
|
||||||
|
body,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
noti.onclick = () => {
|
||||||
|
if (!window.closed) navigate(getInboxInvitesPath());
|
||||||
|
noti.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[navigate]
|
[navigate]
|
||||||
);
|
);
|
||||||
@@ -141,35 +154,69 @@ function MessageNotifications() {
|
|||||||
const notificationSelected = useInboxNotificationsSelected();
|
const notificationSelected = useInboxNotificationsSelected();
|
||||||
const selectedRoomId = useSelectedRoom();
|
const selectedRoomId = useSelectedRoom();
|
||||||
|
|
||||||
|
// Set up notification tap listener for mobile
|
||||||
|
useEffect(() => {
|
||||||
|
setupNotificationTapListener((path) => {
|
||||||
|
navigate(path);
|
||||||
|
});
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
const notify = useCallback(
|
const notify = useCallback(
|
||||||
({
|
({
|
||||||
roomName,
|
roomName,
|
||||||
roomAvatar,
|
roomAvatar,
|
||||||
username,
|
username,
|
||||||
|
messageBody,
|
||||||
|
roomId,
|
||||||
|
eventId,
|
||||||
|
isDm,
|
||||||
}: {
|
}: {
|
||||||
roomName: string;
|
roomName: string;
|
||||||
roomAvatar?: string;
|
roomAvatar?: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
messageBody?: string;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
|
isDm: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const noti = new window.Notification(roomName, {
|
// In DMs, room name is the username, so don't duplicate it
|
||||||
icon: roomAvatar,
|
const body = messageBody
|
||||||
badge: roomAvatar,
|
? (isDm ? messageBody : `${username}: ${messageBody}`)
|
||||||
body: `New inbox notification from ${username}`,
|
: `New message from ${username}`;
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
noti.onclick = () => {
|
// Build the path to navigate to on click
|
||||||
if (!window.closed) navigate(getInboxNotificationsPath());
|
const roomPath = isDm
|
||||||
noti.close();
|
? getDirectRoomPath(roomId, eventId)
|
||||||
notifRef.current = undefined;
|
: getHomeRoomPath(roomId, eventId);
|
||||||
};
|
|
||||||
|
|
||||||
notifRef.current?.close();
|
if (isTauri()) {
|
||||||
notifRef.current = noti;
|
sendNotification({
|
||||||
|
title: roomName,
|
||||||
|
body,
|
||||||
|
path: roomPath,
|
||||||
|
onClick: () => {
|
||||||
|
if (!window.closed) navigate(roomPath);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const noti = new window.Notification(roomName, {
|
||||||
|
icon: roomAvatar,
|
||||||
|
badge: roomAvatar,
|
||||||
|
body,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
noti.onclick = () => {
|
||||||
|
if (!window.closed) navigate(roomPath);
|
||||||
|
noti.close();
|
||||||
|
notifRef.current = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
notifRef.current?.close();
|
||||||
|
notifRef.current = noti;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[navigate]
|
[mx, navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const playSound = useCallback(() => {
|
const playSound = useCallback(() => {
|
||||||
@@ -212,17 +259,22 @@ function MessageNotifications() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showNotifications && notificationPermission('granted')) {
|
if (showNotifications && (isTauri() || notificationPermission('granted'))) {
|
||||||
const avatarMxc =
|
const avatarMxc =
|
||||||
room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl();
|
room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl();
|
||||||
|
const content = mEvent.getContent();
|
||||||
|
const messageBody = typeof content.body === 'string' ? content.body : undefined;
|
||||||
|
const isDm = room.getJoinedMemberCount() === 2 && !room.isSpaceRoom();
|
||||||
notify({
|
notify({
|
||||||
roomName: room.name ?? 'Unknown',
|
roomName: room.name ?? 'Unknown',
|
||||||
roomAvatar: avatarMxc
|
roomAvatar: avatarMxc
|
||||||
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
|
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||||
: undefined,
|
: undefined,
|
||||||
username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender,
|
username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender,
|
||||||
|
messageBody,
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
eventId,
|
eventId,
|
||||||
|
isDm,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
150
src/app/utils/tauri.ts
Normal file
150
src/app/utils/tauri.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* Tauri-specific utilities for desktop and mobile platforms
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Callback for notification tap actions */
|
||||||
|
let notificationTapCallback: ((path: string) => void) | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up listener for notification tap actions on mobile
|
||||||
|
* Call this once at app startup with a navigation callback
|
||||||
|
*/
|
||||||
|
export const setupNotificationTapListener = async (onTap: (path: string) => void): Promise<void> => {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
|
||||||
|
notificationTapCallback = onTap;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { onAction } = await import('@tauri-apps/plugin-notification');
|
||||||
|
|
||||||
|
await onAction((notification) => {
|
||||||
|
// 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 notification tap listener:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're running inside a Tauri application
|
||||||
|
*/
|
||||||
|
export const isTauri = (): boolean =>
|
||||||
|
'__TAURI__' in window || '__TAURI_INTERNALS__' in window;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're running on a mobile platform (Android/iOS)
|
||||||
|
*/
|
||||||
|
export const isTauriMobile = (): boolean => {
|
||||||
|
if (!isTauri()) return false;
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
return ua.includes('android') || ua.includes('iphone') || ua.includes('ipad');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're running on Android
|
||||||
|
*/
|
||||||
|
export const isAndroid = (): boolean => {
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
return ua.includes('android');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply safe area insets for mobile devices
|
||||||
|
* On Android, CSS env() may not work, so we apply fallback padding
|
||||||
|
*/
|
||||||
|
export const applySafeAreaInsets = (): void => {
|
||||||
|
if (!isTauriMobile()) return;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
// Check if env() is working by testing if it returns a value
|
||||||
|
const testValue = getComputedStyle(root).getPropertyValue('--safe-area-inset-top');
|
||||||
|
const envWorking = testValue && testValue !== '0px' && testValue !== '';
|
||||||
|
|
||||||
|
if (!envWorking && isAndroid()) {
|
||||||
|
// Apply fallback padding for Android status bar and navigation bar
|
||||||
|
// Status bar is typically 24-48dp, navigation bar is typically 48dp
|
||||||
|
// We use conservative values that work on most devices
|
||||||
|
root.style.setProperty('--safe-area-inset-top', '28px');
|
||||||
|
root.style.setProperty('--safe-area-inset-bottom', '24px');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a browser notification
|
||||||
|
*/
|
||||||
|
const sendBrowserNotification = (options: {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
icon?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}): Notification | undefined => {
|
||||||
|
const { title, body, icon, onClick } = options;
|
||||||
|
|
||||||
|
if (!('Notification' in window)) return undefined;
|
||||||
|
if (Notification.permission !== 'granted') return undefined;
|
||||||
|
|
||||||
|
const notification = new Notification(title, {
|
||||||
|
body,
|
||||||
|
icon,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
notification.onclick = () => {
|
||||||
|
if (!window.closed) onClick();
|
||||||
|
notification.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a notification using Tauri's notification plugin
|
||||||
|
* Falls back to browser Notification API if not in Tauri
|
||||||
|
*/
|
||||||
|
export const sendNotification = async (options: {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
icon?: string;
|
||||||
|
path?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}): Promise<void> => {
|
||||||
|
const { title, body, icon, path, onClick } = options;
|
||||||
|
|
||||||
|
if (isTauri()) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
sendNotification: tauriSendNotification,
|
||||||
|
isPermissionGranted,
|
||||||
|
requestPermission,
|
||||||
|
} = await import('@tauri-apps/plugin-notification');
|
||||||
|
|
||||||
|
let permissionGranted = await isPermissionGranted();
|
||||||
|
if (!permissionGranted) {
|
||||||
|
const permission = await requestPermission();
|
||||||
|
permissionGranted = permission === 'granted';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionGranted) {
|
||||||
|
await tauriSendNotification({
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
// Store path in extra data for notification tap handling
|
||||||
|
extra: path ? { path } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Tauri notification failed:', err);
|
||||||
|
// Fallback to browser notification on error
|
||||||
|
sendBrowserNotification({ title, body, icon, onClick });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendBrowserNotification({ title, body, icon, onClick });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -41,19 +41,45 @@
|
|||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* Enable safe area variables */
|
||||||
|
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||||
|
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
/* Safe area insets for mobile devices (notch, navigation bar) */
|
||||||
|
padding-top: var(--safe-area-inset-top);
|
||||||
|
padding-bottom: var(--safe-area-inset-bottom);
|
||||||
|
padding-left: var(--safe-area-inset-left);
|
||||||
|
padding-right: var(--safe-area-inset-right);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: var(--font-secondary);
|
font-family: var(--font-secondary);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
/* Default to dark theme background for safe areas */
|
||||||
|
background-color: #1A1A1A;
|
||||||
|
|
||||||
/*Why font-variant-ligatures => https://github.com/rsms/inter/issues/222 */
|
/*Why font-variant-ligatures => https://github.com/rsms/inter/issues/222 */
|
||||||
font-variant-ligatures: no-contextual;
|
font-variant-ligatures: no-contextual;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Theme-specific body backgrounds for safe area padding */
|
||||||
|
body.light-theme {
|
||||||
|
background-color: #F0F0F0;
|
||||||
|
}
|
||||||
|
body.silver-theme {
|
||||||
|
background-color: #DEDEDE;
|
||||||
|
}
|
||||||
|
body.dark-theme {
|
||||||
|
background-color: #1A1A1A;
|
||||||
|
}
|
||||||
|
body.butter-theme {
|
||||||
|
background-color: #1A1916;
|
||||||
|
}
|
||||||
#root {
|
#root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ import './index.css';
|
|||||||
|
|
||||||
import { trimTrailingSlash } from './app/utils/common';
|
import { trimTrailingSlash } from './app/utils/common';
|
||||||
import App from './app/pages/App';
|
import App from './app/pages/App';
|
||||||
|
import { applySafeAreaInsets } from './app/utils/tauri';
|
||||||
|
|
||||||
// import i18n (needs to be bundled ;))
|
// import i18n (needs to be bundled ;))
|
||||||
import './app/i18n';
|
import './app/i18n';
|
||||||
|
|
||||||
document.body.classList.add(configClass, varsClass);
|
document.body.classList.add(configClass, varsClass);
|
||||||
|
|
||||||
|
// Apply safe area insets for mobile devices
|
||||||
|
applySafeAreaInsets();
|
||||||
|
|
||||||
// Register Service Worker
|
// Register Service Worker
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
const swUrl =
|
const swUrl =
|
||||||
|
|||||||
Reference in New Issue
Block a user