mobile: add sync status button to side menu (#10112)

* mobile: add sync status button to side menu

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: minor fixes

* mobile: hide sync status during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: skipped sync during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
This commit is contained in:
Kashaf Ansari
2026-07-30 08:54:05 +05:00
committed by GitHub
parent b5140d9947
commit 6e10ce8d8e
8 changed files with 179 additions and 3 deletions

View File

@@ -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}
/>
))}
<SyncStatusButton />
<SettingsIcon />
</View>
</View>

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 (
<View
style={{
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
{syncing ? (
<View
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border,
justifyContent: "center",
alignItems: "center"
}}
>
<Animated.View style={rotationStyle}>
<AppIcon name="sync" size={AppFontSize.lg} color={getIconColor()} />
</Animated.View>
</View>
) : (
<IconButton
name="sync"
onPress={onPress}
tooltipText={tooltipText}
tooltipPosition={NativeTooltip.POSITIONS.BOTTOM}
size={AppFontSize.lg}
color={getIconColor()}
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border
}}
/>
)}
{!syncing && (isFailed || isOffline) ? (
<AppIcon
name="information"
color={isOffline ? colors.static.yellow : colors.error.icon}
size={AppFontSize.xxs}
style={{ position: "absolute", bottom: -3, right: -3 }}
/>
) : null}
</View>
);
};
export default SyncStatusButton;

View File

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

View File

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

View File

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

View File

@@ -48,6 +48,8 @@ export interface UserStore {
disableAppLockRequests: boolean;
setDisableAppLockRequests: (disableAppLockRequests: boolean) => void;
profile?: Partial<Profile>;
isLoggingOut: boolean;
setIsLoggingOut: (value: boolean) => void;
}
export const useUserStore = create<UserStore>((set) => ({
@@ -95,5 +97,7 @@ export const useUserStore = create<UserStore>((set) => ({
set({ disableAppLockRequests: false });
}, 1000);
},
profile: undefined
profile: undefined,
isLoggingOut: false,
setIsLoggingOut: (value) => set({ isLoggingOut: value })
}));

View File

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