From 0a8f7bdd35a263c53d78bdd331fc111caa6f2a1e Mon Sep 17 00:00:00 2001 From: mykh-hailo Date: Wed, 8 Apr 2026 13:18:46 -0300 Subject: [PATCH 1/8] feat: snack bar to go to target folder on copy/move --- .../android/extensions/ActivityExt.kt | 12 ++++++++ .../ui/activity/FileDisplayActivity.kt | 30 +++++++++++++++++++ owncloudApp/src/main/res/values/strings.xml | 4 +++ 3 files changed, 46 insertions(+) diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt index c1efbf2966c..f1d0d3a4798 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt @@ -90,6 +90,18 @@ fun Activity.showMessageInSnackbar( Snackbar.make(findViewById(layoutId), message, duration).show() } +fun Activity.showSnackbarWithAction( + message: CharSequence, + actionText: CharSequence, + action: () -> Unit, + duration: Int = Snackbar.LENGTH_LONG, + layoutId: Int = android.R.id.content +) { + Snackbar.make(findViewById(layoutId), message, duration) + .setAction(actionText) { action() } + .show() +} + fun Activity.showErrorInToast( genericErrorMessageId: Int, throwable: Throwable?, diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index c849947a10b..ec19abd7759 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -84,6 +84,7 @@ import com.owncloud.android.extensions.parseError import com.owncloud.android.extensions.sendDownloadedFilesByShareSheet import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar +import com.owncloud.android.extensions.showSnackbarWithAction import com.owncloud.android.lib.common.accounts.AccountUtils import com.owncloud.android.lib.common.authentication.OwnCloudBearerCredentials import com.owncloud.android.lib.common.network.CertificateCombinedException @@ -181,6 +182,8 @@ class FileDisplayActivity : FileActivity(), private var waitingToSend: OCFile? = null private var waitingToOpen: OCFile? = null + private var copyMoveTargetFolder: OCFile? = null + private var localBroadcastManager: LocalBroadcastManager? = null private val fileOperationsViewModel: FileOperationsViewModel by viewModel() @@ -707,6 +710,7 @@ class FileDisplayActivity : FileActivity(), private fun requestMoveOperation(data: Intent) { val folderToMoveAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER) ?: return val files = data.getParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES) ?: return + copyMoveTargetFolder = folderToMoveAt val moveOperation = FileOperation.MoveOperation( listOfFilesToMove = files.toList(), targetFolder = folderToMoveAt, @@ -723,6 +727,7 @@ class FileDisplayActivity : FileActivity(), private fun requestCopyOperation(data: Intent) { val folderToCopyAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER) ?: return val files = data.getParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES) ?: return + copyMoveTargetFolder = folderToCopyAt val copyOperation = FileOperation.CopyOperation( listOfFilesToCopy = files.toList(), targetFolder = folderToCopyAt, @@ -1082,6 +1087,9 @@ class FileDisplayActivity : FileActivity(), // Refresh the spaces and update the quota spacesListViewModel.refreshSpacesFromServer() } + if (uiResult.data.isNullOrEmpty()) { + showCopyMoveSuccessSnackbar(isCopy = false) + } } is UIResult.Error -> { @@ -1130,6 +1138,9 @@ class FileDisplayActivity : FileActivity(), // Refresh the spaces and update the quota spacesListViewModel.refreshSpacesFromServer() + if (uiResult.data.isNullOrEmpty()) { + showCopyMoveSuccessSnackbar(isCopy = true) + } } is UIResult.Error -> { @@ -1148,6 +1159,25 @@ class FileDisplayActivity : FileActivity(), } } + private fun showCopyMoveSuccessSnackbar(isCopy: Boolean) { + val message = getString(if (isCopy) R.string.copy_file_correctly else R.string.move_file_correctly) + val targetFolderId = copyMoveTargetFolder?.id + if (targetFolderId != null) { + showSnackbarWithAction( + message = message, + actionText = getString(R.string.go_to_destination_folder), + action = { + val fileListFragment = mainFileListFragment + ?: supportFragmentManager.findFragmentById(R.id.left_fragment_container) as? MainFileListFragment + fileListFragment?.navigateToFolderId(targetFolderId) + }, + layoutId = R.id.list_layout + ) + } else { + showMessageInSnackbar(R.id.list_layout, message) + } + } + private fun showConflictDecisionDialog( uiResult: UIResult.Success>, data: List, diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index d95fa82bc53..6555e02b97e 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -544,14 +544,18 @@ It is not possible to move a folder into a descendant. The file exists already in the destination folder. An error occurred while trying to move this file or folder. + File moved correctly to move this file Unable to copy. Please check whether the file exists. It is not possible to copy a folder into a descendant. The file exists already in the destination folder. An error occurred while trying to copy this file or folder. + File copied correctly to copy this file + Open Folder + Camera uploads Synchronization of %1$s folder could not be completed From d7e87ad296eabaa70c52046621ade679a2f6826c Mon Sep 17 00:00:00 2001 From: mykh-hailo Date: Wed, 8 Apr 2026 13:19:02 -0300 Subject: [PATCH 2/8] chore: add changelog --- changelog/unreleased/4802 | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/unreleased/4802 diff --git a/changelog/unreleased/4802 b/changelog/unreleased/4802 new file mode 100644 index 00000000000..7cb096d68a4 --- /dev/null +++ b/changelog/unreleased/4802 @@ -0,0 +1,6 @@ +Enhancement: Show destination folder snackbar for copy/move operations + +A snackbar message has been displayed after copy or move operations with an action button that allows users to quickly navigate to the destination folder. + +https://github.com/owncloud/android/issues/4379 +https://github.com/owncloud/android/pull/4802 From 336058876dadf80206d924c7f839c9156df2ace9 Mon Sep 17 00:00:00 2001 From: mykh-hailo Date: Wed, 8 Apr 2026 13:20:00 -0300 Subject: [PATCH 3/8] fix: update snack bar to use extensions --- .../android/extensions/FragmentExt.kt | 12 +++++++ .../files/details/FileDetailsFragment.kt | 10 ++++-- .../android/ui/activity/FileActivity.java | 33 ++++++++++++++----- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt index 14db6e7f9da..1187c70a954 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt @@ -51,6 +51,18 @@ fun Fragment.showMessageInSnackbar( Snackbar.make(requiredView, message, duration).show() } +fun Fragment.showSnackbarWithAction( + message: CharSequence, + actionText: CharSequence, + action: () -> Unit, + duration: Int = Snackbar.LENGTH_LONG +) { + val requiredView = view ?: return + Snackbar.make(requiredView, message, duration) + .setAction(actionText) { action() } + .show() +} + fun Fragment.showAlertDialog( title: String, message: String, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt index 91dd7f17e21..4025e742cc3 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt @@ -57,6 +57,7 @@ import com.owncloud.android.extensions.openOCFile import com.owncloud.android.extensions.sendDownloadedFilesByShareSheet import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar +import com.owncloud.android.extensions.showSnackbarWithAction import com.owncloud.android.presentation.authentication.ACTION_UPDATE_EXPIRED_TOKEN import com.owncloud.android.presentation.authentication.EXTRA_ACCOUNT import com.owncloud.android.presentation.authentication.EXTRA_ACTION @@ -168,8 +169,10 @@ class FileDetailsFragment : FileFragment() { when (uiResult) { is UIResult.Error -> { if (uiResult.error is AccountNotFoundException) { - Snackbar.make(view, getString(R.string.sync_fail_ticker_unauthorized), Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.auth_oauth_failure_snackbar_action) { + showSnackbarWithAction( + message = getString(R.string.sync_fail_ticker_unauthorized), + actionText = getString(R.string.auth_oauth_failure_snackbar_action), + action = { val updateAccountCredentials = Intent(requireActivity(), LoginActivity::class.java) updateAccountCredentials.apply { putExtra(EXTRA_ACCOUNT, fileDetailsViewModel.getAccount()) @@ -177,7 +180,8 @@ class FileDetailsFragment : FileFragment() { addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) } startActivity(updateAccountCredentials) - }.show() + }, + duration = Snackbar.LENGTH_INDEFINITE) } else { showErrorInSnackbar(R.string.sync_fail_ticker, uiResult.error) fileDetailsViewModel.updateActionInDetailsView(NONE) diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index 98f7611ace5..863316fa2b5 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -58,8 +58,11 @@ import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; import com.owncloud.android.ui.errorhandling.ErrorMessageAdapter; import com.owncloud.android.ui.helpers.FileOperationsHelper; +import kotlin.Unit; import timber.log.Timber; +import static com.owncloud.android.extensions.ActivityExtKt.showSnackbarWithAction; + /** * Activity with common behaviour for activities handling {@link OCFile}s in ownCloud {@link Account}s . */ @@ -278,18 +281,32 @@ protected void showRequestAccountChangeNotice(String errorMessage, boolean mustC .setCancelable(false) .show(); } else { - Snackbar.make(findViewById(android.R.id.content), errorMessage, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.auth_oauth_failure_snackbar_action, v -> - requestCredentialsUpdate()) - .show(); + showSnackbarWithAction( + this, + errorMessage, + getString(R.string.auth_oauth_failure_snackbar_action), + () -> { + requestCredentialsUpdate(); + return Unit.INSTANCE; + }, + Snackbar.LENGTH_INDEFINITE, + android.R.id.content + ); } } protected void showRequestRegainAccess() { - Snackbar.make(findViewById(android.R.id.content), R.string.auth_oauth_failure, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.auth_oauth_failure_snackbar_action, v -> - requestCredentialsUpdate()) - .show(); + showSnackbarWithAction( + this, + getString(R.string.auth_oauth_failure), + getString(R.string.auth_oauth_failure_snackbar_action), + () -> { + requestCredentialsUpdate(); + return Unit.INSTANCE; + }, + Snackbar.LENGTH_INDEFINITE, + android.R.id.content + ); } /** From 9ecb9a88a56bc7e7223c1d3e7b8331d144442a81 Mon Sep 17 00:00:00 2001 From: mykh-hailo Date: Wed, 8 Apr 2026 13:20:12 -0300 Subject: [PATCH 4/8] fix: remove 3-dot menu on picker mode --- .../android/presentation/files/filelist/FileListAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt index 5e8bb13ddff..fd0cf999b3f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt @@ -288,7 +288,7 @@ class FileListAdapter( params -> params.marginStart = if (isFolderInKw) 0 else context.resources.getDimensionPixelSize(R.dimen.standard_quarter_margin) } it.fileListLastMod.text = DisplayUtils.getRelativeTimestamp(context, file.modificationTimestamp) - it.threeDotMenu.isVisible = getCheckedItems().isEmpty() + it.threeDotMenu.isVisible = !isPickerMode && getCheckedItems().isEmpty() it.threeDotMenu.contentDescription = context.getString(R.string.content_description_file_operations, file.fileName) if (fileListOption.isAvailableOffline() || (fileListOption.isSharedByLink() && fileWithSyncInfo.space == null)) { it.spacePathLine.path.apply { From c8dc71351bf4effe1ff0e53309a7797e085a1f59 Mon Sep 17 00:00:00 2001 From: mykh-hailo Date: Wed, 8 Apr 2026 13:20:28 -0300 Subject: [PATCH 5/8] chore: add release note. --- .../presentation/releasenotes/ReleaseNotesViewModel.kt | 5 +++++ owncloudApp/src/main/res/values/strings.xml | 2 ++ 2 files changed, 7 insertions(+) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt index ea18d0e4b26..47421a2fe46 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt @@ -63,6 +63,11 @@ class ReleaseNotesViewModel( subtitle = R.string.release_notes_4_8_0_subtitle_spaces_permanent_links, type = ReleaseNoteType.ENHANCEMENT ), + ReleaseNote( + title = R.string.release_notes_4_8_0_title_action_to_copy_or_move_destination_folder, + subtitle = R.string.release_notes_4_8_0_subtitle_action_to_copy_or_move_destination_folder, + type = ReleaseNoteType.ENHANCEMENT + ), ReleaseNote( title = R.string.release_notes_bugfixes_title, subtitle = R.string.release_notes_bugfixes_subtitle, diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 6555e02b97e..5cf6bb53f15 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -752,6 +752,8 @@ Infinite Scale users can now get a permanent link for a space and share it with other members Space public links Infinite Scale users can see all public links of a space and manage them with right permissions + Navigation to target folder + New action to navigate to the destination folder when a file or folder is copied or moved Open in web From d36482d2555eb9902861b0e92e91c11f243bbb15 Mon Sep 17 00:00:00 2001 From: mykh-hailo Date: Wed, 8 Apr 2026 13:20:51 -0300 Subject: [PATCH 6/8] fix: update bottom bar selection on snack bar action --- .../android/ui/activity/FileDisplayActivity.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index ec19abd7759..42ac5b0db23 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -813,12 +813,10 @@ class FileDisplayActivity : FileActivity(), Timber.v("onResume() start") super.onResume() + updateBottombar(mainFileListFragment?.getCurrentSpace()) if (mainFileListFragment?.getCurrentSpace()?.isProject == true || (mainFileListFragment?.getCurrentSpace()?.isPersonal == true && isMultiPersonal)) { - setCheckedItemAtBottomBar(getMenuItemForFileListOption(FileListOption.SPACES_LIST)) updateToolbar(null, mainFileListFragment?.getCurrentSpace()) - } else { - setCheckedItemAtBottomBar(getMenuItemForFileListOption(fileListOption)) } if (secondFragment == null) { @@ -1893,9 +1891,20 @@ class FileDisplayActivity : FileActivity(), override fun onCurrentFolderUpdated(newCurrentFolder: OCFile, currentSpace: OCSpace?) { updateToolbar(newCurrentFolder, currentSpace) + val newCurrentFolderSpace = spacesListViewModel.spacesList.value.spaces.find { it.id == newCurrentFolder.spaceId } + updateBottombar(newCurrentFolderSpace) file = newCurrentFolder } + private fun updateBottombar(currentSpace: OCSpace?) { + val bottomBarOption = if (currentSpace?.isProject == true) { + FileListOption.SPACES_LIST + } else { + fileListOption + } + setCheckedItemAtBottomBar(getMenuItemForFileListOption(bottomBarOption)) + } + override fun onFileClicked(file: OCFile) { when { PreviewImageFragment.canBePreviewed(file) -> { From ac33d8c2b339c3614dd85b5d23f1cd9a70356a18 Mon Sep 17 00:00:00 2001 From: mykh-hailo Date: Thu, 9 Apr 2026 05:16:41 -0300 Subject: [PATCH 7/8] fix: update string for multiple files/folders. --- owncloudApp/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 5cf6bb53f15..cc48a4e26a0 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -544,14 +544,14 @@ It is not possible to move a folder into a descendant. The file exists already in the destination folder. An error occurred while trying to move this file or folder. - File moved correctly + Item(s) moved correctly to move this file Unable to copy. Please check whether the file exists. It is not possible to copy a folder into a descendant. The file exists already in the destination folder. An error occurred while trying to copy this file or folder. - File copied correctly + Item(s) copied correctly to copy this file Open Folder From a790a9214d86c78ae70591f0b890b5132258c8e5 Mon Sep 17 00:00:00 2001 From: mykh-hailo Date: Thu, 9 Apr 2026 05:43:31 -0300 Subject: [PATCH 8/8] fix: optimize bottom bar update on snack action. --- .../com/owncloud/android/ui/activity/FileDisplayActivity.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 42ac5b0db23..f42851c0e6d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -1168,6 +1168,10 @@ class FileDisplayActivity : FileActivity(), val fileListFragment = mainFileListFragment ?: supportFragmentManager.findFragmentById(R.id.left_fragment_container) as? MainFileListFragment fileListFragment?.navigateToFolderId(targetFolderId) + val targetFolderSpace = spacesListViewModel.spacesList.value.spaces.find { + it.id == copyMoveTargetFolder?.spaceId + } + updateBottombar(targetFolderSpace) }, layoutId = R.id.list_layout ) @@ -1891,8 +1895,6 @@ class FileDisplayActivity : FileActivity(), override fun onCurrentFolderUpdated(newCurrentFolder: OCFile, currentSpace: OCSpace?) { updateToolbar(newCurrentFolder, currentSpace) - val newCurrentFolderSpace = spacesListViewModel.spacesList.value.spaces.find { it.id == newCurrentFolder.spaceId } - updateBottombar(newCurrentFolderSpace) file = newCurrentFolder }