From 9d9209a14954079329af095bddc8380d939068d8 Mon Sep 17 00:00:00 2001 From: Siddarth Kumar Date: Sat, 3 Oct 2026 21:19:50 +0530 Subject: [PATCH 1/2] android: improve messaging around SAF picker --- mobile-app/src/screens/AddDeviceModal.tsx | 9 ++++++--- mobile-app/src/screens/DevicesScreen.tsx | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/mobile-app/src/screens/AddDeviceModal.tsx b/mobile-app/src/screens/AddDeviceModal.tsx index 50a6aaa..1d88e6d 100644 --- a/mobile-app/src/screens/AddDeviceModal.tsx +++ b/mobile-app/src/screens/AddDeviceModal.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { ActivityIndicator, + Platform, StyleSheet, Switch, Text, @@ -40,7 +41,7 @@ export function AddDeviceModal({ visible, onClose, onAdded, initialDeviceId }: P // accept fresh pre-fills when reopened (e.g. successive QR scans) if (visible && initialDeviceId) setDeviceId(initialDeviceId); }, [visible, initialDeviceId]); - const [autoAcceptFolders, setAutoAcceptFolders] = useState(true); + const [autoAcceptFolders, setAutoAcceptFolders] = useState(Platform.OS !== 'android'); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); const [scannerOpen, setScannerOpen] = useState(false); @@ -74,7 +75,7 @@ export function AddDeviceModal({ visible, onClose, onAdded, initialDeviceId }: P const reset = () => { setDeviceId(''); setName(''); - setAutoAcceptFolders(true); + setAutoAcceptFolders(Platform.OS !== 'android'); setError(null); setSubmitting(false); }; @@ -184,7 +185,9 @@ export function AddDeviceModal({ visible, onClose, onAdded, initialDeviceId }: P Auto-accept folders - Skip the per-folder accept prompt for offers from this peer. + {Platform.OS === 'android' + ? 'Skip the per-folder prompt. Folders land in app storage (Android/data), which other apps cannot open.' + : 'Skip the per-folder accept prompt for offers from this peer.'} { if (busy) return; @@ -205,7 +206,9 @@ function PendingDeviceCard({ Auto-accept folders - Skip the per-folder accept prompt for offers from this peer. + {Platform.OS === 'android' + ? 'Skip the per-folder prompt. Folders land in app storage (Android/data), which other apps cannot open.' + : 'Skip the per-folder accept prompt for offers from this peer.'} Date: Sun, 4 Oct 2026 17:51:44 +0530 Subject: [PATCH 2/2] ui: allow moving folders --- mobile-app/src/screens/FolderDetailModal.tsx | 101 +++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/mobile-app/src/screens/FolderDetailModal.tsx b/mobile-app/src/screens/FolderDetailModal.tsx index be1b104..351c505 100644 --- a/mobile-app/src/screens/FolderDetailModal.tsx +++ b/mobile-app/src/screens/FolderDetailModal.tsx @@ -19,10 +19,12 @@ import { Focusable } from '../components/Focusable'; import { useKeyboardHeight } from '../hooks/useKeyboardHeight'; import { useSyncthing, useSyncthingClient } from '../daemon/SyncthingContext'; import type { DbStatus, DeviceConfig, FolderConfig, FolderError } from '../api/types'; +import { isAbortError } from '../api/syncthing'; import { colors, formatBytes, Progress } from '../components/ui'; import { removeDir } from '../fs/bridgeFs'; import GoBridge from '../GoServerBridgeJSI'; import { + filesystemTypeForExternal, isExternalFolder, pickExternalFolderWithICloudWarning, externalFolderNeedsAllFilesAccess, @@ -401,6 +403,95 @@ export function FolderDetailModal({ } }; + const moveReady = !!status && status.state === 'idle' && status.needBytes === 0; + + const doMove = async (newPath: string, displayName: string) => { + if (!folder) return; + setBusy(true); + setError(null); + const oldPath = folder.path; + try { + let ignores: string[] = []; + try { + ignores = await client.getIgnores(folder.id); + } catch { + // keep going; folder still usable without ignores + } + await client.deleteFolder(folder.id); + const fsType = filesystemTypeForExternal(newPath); + const usesSaf = fsType === 'saf'; + const moved: FolderConfig = { + ...folder, + path: newPath, + filesystemType: fsType, + rescanIntervalS: usesSaf ? 60 : folder.rescanIntervalS, + fsWatcherEnabled: !usesSaf && folder.fsWatcherEnabled, + }; + try { + await client.putFolder(moved); + } catch (e) { + if (!isAbortError(e)) throw e; + const created = await client.waitForFolder(moved.id, { deadlineMs: 60_000 }); + if (!created) throw new Error('Moving the folder timed out. Please try again.'); + } + if (ignores.length > 0) { + try { + await client.setIgnores(moved.id, ignores); + } catch { + // best-effort + } + } + onChanged(); + onClose(); + if (isExternal) return; + Alert.alert( + 'Folder moved', + `"${folder.label || folder.id}" now syncs to ${displayName}. Files will be re-downloaded from your other devices.\n\nDelete the old copy in app storage?`, + [ + { text: 'Keep', style: 'cancel' }, + { + text: 'Delete old copy', + style: 'destructive', + onPress: () => { + try { + removeDir(oldPath); + } catch (e) { + Alert.alert('Could not delete', e instanceof Error ? e.message : String(e)); + } + }, + }, + ], + ); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setBusy(false); + } + }; + + const confirmMove = () => { + if (!folder) return; + if (!moveReady) { + Alert.alert( + 'Folder not up to date', + 'Wait until this folder shows "Up to date" before moving it, otherwise local changes that have not synced yet will be lost.', + ); + return; + } + pickExternalFolderWithICloudWarning(picked => { + if (!picked) return; + const name = picked.displayName || 'the chosen folder'; + Alert.alert( + 'Move folder?', + `"${folder.label || folder.id}" will sync to ${name} instead of its current location.\n\nAll files will be re-downloaded from your other devices. The folder must stay shared with at least one connected device.`, + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Move', onPress: () => doMove(picked.path, name) }, + ], + ); + }); + }; + const confirmDelete = () => { if (!folder) return; Alert.alert( @@ -944,6 +1035,16 @@ export function FolderDetailModal({ ) : null} + + + Move to another location + + +