diff --git a/apps/mobile/app/components/side-menu/side-menu-header.tsx b/apps/mobile/app/components/side-menu/side-menu-header.tsx index 1486b7973..3ca0b5f91 100644 --- a/apps/mobile/app/components/side-menu/side-menu-header.tsx +++ b/apps/mobile/app/components/side-menu/side-menu-header.tsx @@ -30,6 +30,7 @@ import { Pressable } from "../ui/pressable"; import { SvgView } from "../ui/svg"; import Heading from "../ui/typography/heading"; import { useSideBarDraggingStore } from "./dragging-store"; +import SyncStatusButton from "./sync-status-button"; const SettingsIcon = () => { const { colors } = useThemeColors(); @@ -123,7 +124,7 @@ export const SideMenuHeader = (props: { rightButtons?: IconButtonProps[] }) => { size={AppFontSize.lg} /> ))} - + diff --git a/apps/mobile/app/components/side-menu/sync-status-button.tsx b/apps/mobile/app/components/side-menu/sync-status-button.tsx new file mode 100644 index 000000000..9aa51170c --- /dev/null +++ b/apps/mobile/app/components/side-menu/sync-status-button.tsx @@ -0,0 +1,161 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +import { useTimeAgo } from "@notesnook/common"; +import { strings } from "@notesnook/intl"; +import { useThemeColors } from "@notesnook/theme"; +import { useNetInfo } from "@react-native-community/netinfo"; +import React from "react"; +import { View } from "react-native"; +import Animated, { + Easing, + cancelAnimation, + useAnimatedStyle, + useSharedValue, + withRepeat, + withTiming +} from "react-native-reanimated"; +import Sync from "../../services/sync"; +import { SyncStatus, useUserStore } from "../../stores/use-user-store"; +import { AppFontSize } from "../../utils/size"; +import NativeTooltip from "../../utils/tooltip"; +import AppIcon from "../ui/AppIcon"; +import { IconButton } from "../ui/icon-button"; + +const SyncStatusButton = () => { + const { colors } = useThemeColors(); + const [user, syncing, lastSyncStatus, lastSynced, isLoggingOut] = + useUserStore((state) => [ + state.user, + state.syncing, + state.lastSyncStatus, + state.lastSynced, + state.isLoggingOut + ]); + + const { isInternetReachable } = useNetInfo(); + const isOffline = !isInternetReachable; + const hasSyncedBefore = lastSynced && lastSynced !== "Never"; + + const isFailed = lastSyncStatus === SyncStatus.Failed; + const isSynced = lastSyncStatus === SyncStatus.Passed; + const lastSyncedTimeAgo = useTimeAgo(lastSynced, { + interval: 5000, + live: true + }); + + const getIconColor = (): string => { + if (syncing) return colors.primary.accent; + if (isSynced) return colors.primary.accent; + return colors.secondary.icon; + }; + + const rotation = useSharedValue(0); + + React.useEffect(() => { + if (syncing && !isOffline) { + rotation.value = 0; + rotation.value = withRepeat( + withTiming(360, { duration: 1000, easing: Easing.linear }), + -1, + false + ); + } else { + cancelAnimation(rotation); + rotation.value = withTiming(360, { + duration: 1000 * (1 - (rotation.value % 360) / 360), + easing: Easing.linear + }); + } + }, [syncing, rotation, isOffline]); + + const rotationStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${rotation.value}deg` }] + })); + + const tooltipText = React.useMemo(() => { + const offlineSuffix = isOffline ? ` (${strings.offline()})` : ""; + + if (syncing) return strings.syncing(); + if (!hasSyncedBefore) return `${strings.never()}${offlineSuffix}`; + if (isFailed) return `${strings.syncFailed()}${offlineSuffix}`; + return `${strings.synced()} • ${lastSyncedTimeAgo}${offlineSuffix ? ` • ${offlineSuffix}` : ""}`; + }, [isOffline, syncing, hasSyncedBefore, isFailed, lastSyncedTimeAgo]); + + const onPress = () => { + if (syncing) return; + Sync.run(); + }; + + if (!user || isLoggingOut) return null; + + return ( + + {syncing ? ( + + + + + + ) : ( + + )} + + {!syncing && (isFailed || isOffline) ? ( + + ) : null} + + ); +}; + +export default SyncStatusButton; diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 84bbd2e22..91dbdde97 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -345,6 +345,7 @@ const onLogout = async (reason: string) => { SettingsService.resetSettings(); useUserStore.getState().setUser(null); useUserStore.getState().setSyncing(false); + useUserStore.getState().setIsLoggingOut(false); eSendEvent(eAfterSync); }; diff --git a/apps/mobile/app/screens/settings/logout.ts b/apps/mobile/app/screens/settings/logout.ts index bb2936681..fa1e379a0 100644 --- a/apps/mobile/app/screens/settings/logout.ts +++ b/apps/mobile/app/screens/settings/logout.ts @@ -28,6 +28,7 @@ import { } from "../../components/dialogs/progress"; import Navigation from "../../services/navigation"; import BackupService from "../../services/backup"; +import { useUserStore } from "../../stores/use-user-store"; export async function logoutUser() { const hasUnsyncedChanges = await db.hasUnsyncedChanges(); @@ -47,6 +48,7 @@ export async function logoutUser() { : undefined, positivePress: async (_, takeBackup) => { eSendEvent(eCloseSimpleDialog); + useUserStore.getState().setIsLoggingOut(true); setTimeout(async () => { try { startProgress({ @@ -104,6 +106,7 @@ export async function logoutUser() { DatabaseLogger.error(e); ToastManager.error(e as Error, strings.logoutError()); endProgress(); + useUserStore.getState().setIsLoggingOut(false); } }, 300); } diff --git a/apps/mobile/app/services/sync.ts b/apps/mobile/app/services/sync.ts index 33c7c8c73..555062d68 100644 --- a/apps/mobile/app/services/sync.ts +++ b/apps/mobile/app/services/sync.ts @@ -63,6 +63,11 @@ const run = async ( clearTimeout(syncTimer); syncTimer = setTimeout(async () => { + if (useUserStore.getState().isLoggingOut) { + DatabaseLogger.info("Sync skipped — user is logging out"); + return; + } + const userstore = useUserStore.getState(); userstore.setSyncing(true); const user = await db.user.getUser(); diff --git a/apps/mobile/app/stores/use-user-store.ts b/apps/mobile/app/stores/use-user-store.ts index 80431d870..217175179 100644 --- a/apps/mobile/app/stores/use-user-store.ts +++ b/apps/mobile/app/stores/use-user-store.ts @@ -48,6 +48,8 @@ export interface UserStore { disableAppLockRequests: boolean; setDisableAppLockRequests: (disableAppLockRequests: boolean) => void; profile?: Partial; + isLoggingOut: boolean; + setIsLoggingOut: (value: boolean) => void; } export const useUserStore = create((set) => ({ @@ -95,5 +97,7 @@ export const useUserStore = create((set) => ({ set({ disableAppLockRequests: false }); }, 1000); }, - profile: undefined + profile: undefined, + isLoggingOut: false, + setIsLoggingOut: (value) => set({ isLoggingOut: value }) })); diff --git a/apps/mobile/fonts/MaterialCommunityIcons.ttf b/apps/mobile/fonts/MaterialCommunityIcons.ttf index e863c181f..221e890c6 100644 Binary files a/apps/mobile/fonts/MaterialCommunityIcons.ttf and b/apps/mobile/fonts/MaterialCommunityIcons.ttf differ diff --git a/apps/mobile/scripts/optimize-fonts.mjs b/apps/mobile/scripts/optimize-fonts.mjs index 9bf4890a7..438740245 100644 --- a/apps/mobile/scripts/optimize-fonts.mjs +++ b/apps/mobile/scripts/optimize-fonts.mjs @@ -128,7 +128,8 @@ const EXTRA_ICON_NAMES = [ "identifier", "image-area", "clock-outline", - "delete-sweep-outline" + "delete-sweep-outline", + "sync" ]; const __filename = fileURLToPath(import.meta.url);