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);