From 8139f22a7b5bc3532639efde9b7b50dee37010d8 Mon Sep 17 00:00:00 2001 From: joragua Date: Wed, 14 Jan 2026 12:37:05 +0100 Subject: [PATCH 01/19] feat: show "+" icon for adding new members only for space managers --- .../spaces/members/SpaceMembersFragment.kt | 17 ++++++++++++++++- .../spaces/members/SpaceMembersViewModel.kt | 17 ++++++++++++++++- .../src/main/res/layout/members_fragment.xml | 17 ++++++++++++++++- owncloudApp/src/main/res/values/strings.xml | 1 + 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt index 00ed053774d..11349fd8603 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt @@ -3,7 +3,7 @@ * * @author Jorge Aguado Recio * - * Copyright (C) 2025 ownCloud GmbH. + * Copyright (C) 2026 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -96,6 +96,20 @@ class SpaceMembersFragment : Fragment() { } } + collectLatestLifecycleFlow(spaceMembersViewModel.spacePermissions) { event -> + event?.let { + when (val uiResult = event.peekContent()) { + is UIResult.Success -> { + uiResult.data?.let { spacePermissions -> + if (DRIVES_CREATE_PERMISSION in spacePermissions) { binding.addMemberButton.visibility = View.VISIBLE } + } + } + is UIResult.Loading -> { } + is UIResult.Error -> { } + } + } + } + val currentSpace = requireArguments().getParcelable(ARG_CURRENT_SPACE) ?: return binding.apply { itemName.text = currentSpace.name @@ -118,6 +132,7 @@ class SpaceMembersFragment : Fragment() { companion object { private const val ARG_CURRENT_SPACE = "CURRENT_SPACE" private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME" + private const val DRIVES_CREATE_PERMISSION = "libre.graph/driveItem/permissions/create" fun newInstance( accountName: String, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt index 72b4fa673fb..8fb7d58a54d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt @@ -3,7 +3,7 @@ * * @author Jorge Aguado Recio * - * Copyright (C) 2025 ownCloud GmbH. + * Copyright (C) 2026 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -26,6 +26,7 @@ import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.domain.spaces.model.SpaceMembers import com.owncloud.android.domain.spaces.usecases.GetSpaceMembersUseCase import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase +import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase import com.owncloud.android.domain.utils.Event import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.presentation.common.UIResult @@ -36,6 +37,7 @@ import kotlinx.coroutines.flow.StateFlow class SpaceMembersViewModel( private val getRolesAsyncUseCase: GetRolesAsyncUseCase, private val getSpaceMembersUseCase: GetSpaceMembersUseCase, + private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase, private val accountName: String, private val space: OCSpace, private val coroutineDispatcherProvider: CoroutinesDispatcherProvider @@ -47,6 +49,9 @@ class SpaceMembersViewModel( private val _spaceMembers = MutableStateFlow>?>(null) val spaceMembers: StateFlow>?> = _spaceMembers + private val _spacePermissions = MutableStateFlow>>?>(null) + val spacePermissions: StateFlow>>?> = _spacePermissions + init { runUseCaseWithResult( coroutineDispatcher = coroutineDispatcherProvider.io, @@ -56,6 +61,16 @@ class SpaceMembersViewModel( showLoading = false, requiresConnection = true ) + + runUseCaseWithResult( + coroutineDispatcher = coroutineDispatcherProvider.io, + flow = _spacePermissions, + useCase = getSpacePermissionsAsyncUseCase, + useCaseParams = GetSpacePermissionsAsyncUseCase.Params(accountName = accountName, spaceId = space.id), + showLoading = false, + requiresConnection = true + ) + } fun getSpaceMembers() = runUseCaseWithResult( diff --git a/owncloudApp/src/main/res/layout/members_fragment.xml b/owncloudApp/src/main/res/layout/members_fragment.xml index f1fe488b450..51fa77b6f4e 100644 --- a/owncloudApp/src/main/res/layout/members_fragment.xml +++ b/owncloudApp/src/main/res/layout/members_fragment.xml @@ -1,7 +1,7 @@ - + + + + + + + + + + - - + android:layout_height="match_parent" + android:background="@color/actionbar_start_color"> - + - - - - - - - - - - - - - + android:layout_height="0dp" + android:layout_weight="1" + android:scrollbars="vertical" + android:visibility="visible" + tools:visibility="visible"/> From ed2d5d44a8635e427322a0f496ca8f5d2d724c9b Mon Sep 17 00:00:00 2001 From: joragua Date: Thu, 15 Jan 2026 09:41:02 +0100 Subject: [PATCH 03/19] feat: create and display the new layout to add members --- .../spaces/members/AddMemberFragment.kt | 55 +++++++++++++++++++ .../spaces/members/SpaceMembersActivity.kt | 11 +++- .../spaces/members/SpaceMembersFragment.kt | 28 ++++++++++ .../main/res/layout/add_member_fragment.xml | 51 +++++++++++++++++ owncloudApp/src/main/res/values/strings.xml | 1 + 5 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt create mode 100644 owncloudApp/src/main/res/layout/add_member_fragment.xml diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt new file mode 100644 index 00000000000..ad9b982778e --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt @@ -0,0 +1,55 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 . + */ + +package com.owncloud.android.presentation.spaces.members + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.owncloud.android.R +import com.owncloud.android.databinding.AddMemberFragmentBinding + +class AddMemberFragment: Fragment() { + private var _binding: AddMemberFragmentBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = AddMemberFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.searchBar.requestFocus() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + requireActivity().setTitle(R.string.add_member) + } + + companion object { + fun newInstance(): AddMemberFragment { + return AddMemberFragment() + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt index 599d5c0a555..76fe9cf798a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt @@ -30,7 +30,7 @@ import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.utils.DisplayUtils -class SpaceMembersActivity: FileActivity() { +class SpaceMembersActivity: FileActivity(), SpaceMembersFragment.SpaceMemberFragmentListener { private lateinit var binding: MembersActivityBinding @@ -80,8 +80,17 @@ class SpaceMembersActivity: FileActivity() { super.onOptionsItemSelected(item) } + override fun addMember(space: OCSpace) { + supportFragmentManager.transaction { + val fragment = AddMemberFragment.newInstance() + replace(R.id.members_fragment_container, fragment, TAG_ADD_MEMBER_FRAGMENT) + addToBackStack(null) + } + } + companion object { private const val TAG_SPACE_MEMBERS_FRAGMENT = "SPACE_MEMBERS_FRAGMENT" + private const val TAG_ADD_MEMBER_FRAGMENT ="ADD_MEMBER_FRAGMENT" const val EXTRA_SPACE = "EXTRA_SPACE" } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt index 88c28eb9455..a57ed533693 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt @@ -20,6 +20,7 @@ package com.owncloud.android.presentation.spaces.members +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -27,6 +28,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R import com.owncloud.android.databinding.MembersFragmentBinding import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.spaces.model.OCSpace @@ -34,6 +36,7 @@ import com.owncloud.android.extensions.collectLatestLifecycleFlow import com.owncloud.android.presentation.common.UIResult import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import timber.log.Timber class SpaceMembersFragment : Fragment() { private var _binding: MembersFragmentBinding? = null @@ -50,6 +53,7 @@ class SpaceMembersFragment : Fragment() { private lateinit var recyclerView: RecyclerView private var roles: List = emptyList() + private var listener: SpaceMemberFragmentListener? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = MembersFragmentBinding.inflate(inflater, container, false) @@ -107,6 +111,30 @@ class SpaceMembersFragment : Fragment() { } } } + + val currentSpace = requireArguments().getParcelable(ARG_CURRENT_SPACE) ?: return + binding.addMemberButton.setOnClickListener { + listener?.addMember(currentSpace) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + requireActivity().setTitle(R.string.space_members_label) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + try { + listener = context as SpaceMemberFragmentListener? + } catch (e: ClassCastException) { + Timber.e(e, "The activity attached does not implement SpaceMemberFragmentListener") + throw ClassCastException(activity.toString() + " must implement SpaceMemberFragmentListener") + } + } + + interface SpaceMemberFragmentListener { + fun addMember(space: OCSpace) } companion object { diff --git a/owncloudApp/src/main/res/layout/add_member_fragment.xml b/owncloudApp/src/main/res/layout/add_member_fragment.xml new file mode 100644 index 00000000000..fe412782e9c --- /dev/null +++ b/owncloudApp/src/main/res/layout/add_member_fragment.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index b1fd0a0a5a8..7b80d4a01d3 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -878,6 +878,7 @@ Space could not be deleted Set icon Members + Add member forum or contribute in our GitHub repo]]> From 699b901b63d1bcd110415fbc02aabee92d3ff578 Mon Sep 17 00:00:00 2001 From: joragua Date: Fri, 16 Jan 2026 15:03:54 +0100 Subject: [PATCH 04/19] feat: implement logic to search new users --- .../RemoteDataSourceModule.kt | 5 +- .../dependecyinjection/RepositoryModule.kt | 5 +- .../dependecyinjection/UseCaseModule.kt | 6 +- .../spaces/members/AddMemberFragment.kt | 89 ++++++++++++++++++- .../spaces/members/SearchMembersAdapter.kt | 71 +++++++++++++++ .../spaces/members/SpaceMembersActivity.kt | 2 +- .../spaces/members/SpaceMembersViewModel.kt | 19 ++++ .../src/main/res/layout/member_item.xml | 6 +- owncloudApp/src/main/res/values/strings.xml | 2 + .../members/SearchRemoteUsersOperation.kt | 83 +++++++++++++++++ .../members/responses/MembersResponse.kt | 35 ++++++++ .../members/services/MembersService.kt | 29 ++++++ .../members/services/OCMembersService.kt | 33 +++++++ .../owncloud/android/data/ClientManager.kt | 9 +- .../datasources/RemoteMembersDataSource.kt | 27 ++++++ .../OCRemoteMembersDataSource.kt | 46 ++++++++++ .../members/repository/OCMembersRepository.kt | 34 +++++++ .../domain/members/MembersRepository.kt | 27 ++++++ .../android/domain/members/model/OCMember.kt | 27 ++++++ .../members/usecases/SearchUsersUseCase.kt | 34 +++++++ 20 files changed, 579 insertions(+), 10 deletions(-) create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SearchMembersAdapter.kt create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/members/SearchRemoteUsersOperation.kt create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/members/responses/MembersResponse.kt create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/members/services/MembersService.kt create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/members/services/OCMembersService.kt create mode 100644 owncloudData/src/main/java/com/owncloud/android/data/members/datasources/RemoteMembersDataSource.kt create mode 100644 owncloudData/src/main/java/com/owncloud/android/data/members/datasources/implementation/OCRemoteMembersDataSource.kt create mode 100644 owncloudData/src/main/java/com/owncloud/android/data/members/repository/OCMembersRepository.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/members/MembersRepository.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/members/model/OCMember.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/members/usecases/SearchUsersUseCase.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt index a4fb587d19a..14efaaac634 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt @@ -4,7 +4,7 @@ * @author David González Verdugo * @author Jorge Aguado Recio * - * Copyright (C) 2025 ownCloud GmbH. + * Copyright (C) 2026 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -33,6 +33,8 @@ import com.owncloud.android.data.capabilities.datasources.implementation.OCRemot import com.owncloud.android.data.capabilities.datasources.mapper.RemoteCapabilityMapper import com.owncloud.android.data.files.datasources.RemoteFileDataSource import com.owncloud.android.data.files.datasources.implementation.OCRemoteFileDataSource +import com.owncloud.android.data.members.datasources.RemoteMembersDataSource +import com.owncloud.android.data.members.datasources.implementation.OCRemoteMembersDataSource import com.owncloud.android.data.oauth.datasources.RemoteOAuthDataSource import com.owncloud.android.data.oauth.datasources.implementation.OCRemoteOAuthDataSource import com.owncloud.android.data.roles.datasources.RemoteRolesDataSource @@ -76,6 +78,7 @@ val remoteDataSourceModule = module { singleOf(::OCRemoteAuthenticationDataSource) bind RemoteAuthenticationDataSource::class singleOf(::OCRemoteCapabilitiesDataSource) bind RemoteCapabilitiesDataSource::class singleOf(::OCRemoteFileDataSource) bind RemoteFileDataSource::class + singleOf(::OCRemoteMembersDataSource) bind RemoteMembersDataSource::class singleOf(::OCRemoteOAuthDataSource) bind RemoteOAuthDataSource::class singleOf(::OCRemoteRolesDataSource) bind RemoteRolesDataSource::class singleOf(::OCRemoteServerInfoDataSource) bind RemoteServerInfoDataSource::class diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt index ae4ea9fcf79..44e1e762781 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt @@ -6,7 +6,7 @@ * @author Juan Carlos Garrote Gascón * @author Jorge Aguado Recio * - * Copyright (C) 2025 ownCloud GmbH. + * Copyright (C) 2026 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -28,6 +28,7 @@ import com.owncloud.android.data.authentication.repository.OCAuthenticationRepos import com.owncloud.android.data.capabilities.repository.OCCapabilityRepository import com.owncloud.android.data.files.repository.OCFileRepository import com.owncloud.android.data.folderbackup.repository.OCFolderBackupRepository +import com.owncloud.android.data.members.repository.OCMembersRepository import com.owncloud.android.data.oauth.repository.OCOAuthRepository import com.owncloud.android.data.roles.repository.OCRolesRepository import com.owncloud.android.data.server.repository.OCServerInfoRepository @@ -43,6 +44,7 @@ import com.owncloud.android.domain.authentication.oauth.OAuthRepository import com.owncloud.android.domain.automaticuploads.FolderBackupRepository import com.owncloud.android.domain.capabilities.CapabilityRepository import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.domain.members.MembersRepository import com.owncloud.android.domain.roles.RolesRepository import com.owncloud.android.domain.server.ServerInfoRepository import com.owncloud.android.domain.sharing.sharees.ShareeRepository @@ -61,6 +63,7 @@ val repositoryModule = module { factoryOf(::OCCapabilityRepository) bind CapabilityRepository::class factoryOf(::OCFileRepository) bind FileRepository::class factoryOf(::OCFolderBackupRepository) bind FolderBackupRepository::class + factoryOf(::OCMembersRepository) bind MembersRepository::class factoryOf(::OCOAuthRepository) bind OAuthRepository::class factoryOf(::OCRolesRepository) bind RolesRepository::class factoryOf(::OCServerInfoRepository) bind ServerInfoRepository::class diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 90299a510ee..5758eeb2456 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -7,7 +7,7 @@ * @author Aitor Ballesteros Pavón * @author Jorge Aguado Recio * - * Copyright (C) 2025 ownCloud GmbH. + * Copyright (C) 2026 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -107,6 +107,7 @@ import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAc import com.owncloud.android.domain.spaces.usecases.GetSpacesFromEveryAccountUseCaseAsStream import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase import com.owncloud.android.domain.spaces.usecases.GetSpaceMembersUseCase +import com.owncloud.android.domain.members.usecases.SearchUsersUseCase import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransferByIdUseCase import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsStreamUseCase @@ -306,4 +307,7 @@ val useCaseModule = module { // Roles factoryOf(::GetRolesAsyncUseCase) + + // Members + factoryOf(::SearchUsersUseCase) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt index ad9b982778e..efbd789933a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt @@ -25,13 +25,34 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.owncloud.android.R import com.owncloud.android.databinding.AddMemberFragmentBinding +import com.owncloud.android.domain.members.model.OCMember +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.extensions.collectLatestLifecycleFlow +import com.owncloud.android.extensions.showErrorInSnackbar +import com.owncloud.android.presentation.common.UIResult +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf class AddMemberFragment: Fragment() { private var _binding: AddMemberFragmentBinding? = null private val binding get() = _binding!! + private val spaceMembersViewModel: SpaceMembersViewModel by viewModel { + parametersOf( + requireArguments().getString(ARG_ACCOUNT_NAME), + requireArguments().getParcelable(ARG_CURRENT_SPACE) + ) + } + + private lateinit var searchMembersAdapter: SearchMembersAdapter + private lateinit var recyclerView: RecyclerView + + private var listOfUsers = emptyList() + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = AddMemberFragmentBinding.inflate(inflater, container, false) return binding.root @@ -39,7 +60,57 @@ class AddMemberFragment: Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.searchBar.requestFocus() + searchMembersAdapter = SearchMembersAdapter() + recyclerView = binding.membersRecyclerView + recyclerView.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = searchMembersAdapter + } + + collectLatestLifecycleFlow(spaceMembersViewModel.users) { event -> + event?.let { + when (val uiResult = event.peekContent()) { + is UIResult.Success -> { + uiResult.data?.let { + listOfUsers = it + spaceMembersViewModel.getSpaceMembers() + } + } + is UIResult.Loading -> { } + is UIResult.Error -> { + showErrorInSnackbar(R.string.search_members_failed, uiResult.error) + } + } + } + } + + collectLatestLifecycleFlow(spaceMembersViewModel.spaceMembers) { event -> + event?.let { + when (val uiResult = event.peekContent()) { + is UIResult.Success -> { + uiResult.data?.let { + listOfUsers = listOfUsers.filter { user -> !it.members.any { member -> member.id == "u:${user.id}" } } + searchMembersAdapter.addUserMembers(listOfUsers) + } + } + is UIResult.Loading -> { } + is UIResult.Error -> { } + } + } + } + + + binding.searchBar.apply { + requestFocus() + setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean = true + + override fun onQueryTextChange(newText: String): Boolean { + if (newText.length > 2) { spaceMembersViewModel.searchUsers(newText) } else { spaceMembersViewModel.clearSearch() } + return true + } + }) + } } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -48,8 +119,20 @@ class AddMemberFragment: Fragment() { } companion object { - fun newInstance(): AddMemberFragment { - return AddMemberFragment() + private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME" + private const val ARG_CURRENT_SPACE = "CURRENT_SPACE" + + fun newInstance( + accountName: String, + currentSpace: OCSpace + ): AddMemberFragment { + val args = Bundle().apply { + putString(ARG_ACCOUNT_NAME, accountName) + putParcelable(ARG_CURRENT_SPACE, currentSpace) + } + return AddMemberFragment().apply { + arguments = args + } } } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SearchMembersAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SearchMembersAdapter.kt new file mode 100644 index 00000000000..07b375f81fe --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SearchMembersAdapter.kt @@ -0,0 +1,71 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 . + */ + +package com.owncloud.android.presentation.spaces.members + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R +import com.owncloud.android.databinding.MemberItemBinding +import com.owncloud.android.domain.members.model.OCMember +import com.owncloud.android.utils.PreferenceUtils + +class SearchMembersAdapter: RecyclerView.Adapter() { + + private var members: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchMembersViewHolder { + val inflater = LayoutInflater.from(parent.context) + + val view = inflater.inflate(R.layout.member_item, parent, false) + view.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(parent.context) + + return SearchMembersViewHolder(view) + } + + override fun onBindViewHolder(holder: SearchMembersViewHolder, position: Int) { + val member = members[position] + + holder.binding.apply { + memberIcon.setImageResource(R.drawable.ic_user) + memberName.text = member.displayName + memberName.contentDescription = holder.itemView.context.getString(R.string.content_description_member_user, member.displayName) + memberRole.text = if (member.surname == USER_SURNAME) holder.itemView.context.getString(R.string.member_user) else member.surname + } + } + + override fun getItemCount(): Int = members.size + + fun addUserMembers(members: List) { + this.members = members + notifyDataSetChanged() + } + + class SearchMembersViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val binding = MemberItemBinding.bind(itemView) + } + + companion object { + private const val USER_SURNAME = "User" + } + +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt index 76fe9cf798a..a7ed783a306 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt @@ -82,7 +82,7 @@ class SpaceMembersActivity: FileActivity(), SpaceMembersFragment.SpaceMemberFrag override fun addMember(space: OCSpace) { supportFragmentManager.transaction { - val fragment = AddMemberFragment.newInstance() + val fragment = AddMemberFragment.newInstance(account.name, space) replace(R.id.members_fragment_container, fragment, TAG_ADD_MEMBER_FRAGMENT) addToBackStack(null) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt index 8fb7d58a54d..b6f6662a1fd 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersViewModel.kt @@ -21,12 +21,14 @@ package com.owncloud.android.presentation.spaces.members import androidx.lifecycle.ViewModel +import com.owncloud.android.domain.members.model.OCMember import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.domain.spaces.model.SpaceMembers import com.owncloud.android.domain.spaces.usecases.GetSpaceMembersUseCase import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase +import com.owncloud.android.domain.members.usecases.SearchUsersUseCase import com.owncloud.android.domain.utils.Event import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.presentation.common.UIResult @@ -38,6 +40,7 @@ class SpaceMembersViewModel( private val getRolesAsyncUseCase: GetRolesAsyncUseCase, private val getSpaceMembersUseCase: GetSpaceMembersUseCase, private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase, + private val searchUsersUseCase: SearchUsersUseCase, private val accountName: String, private val space: OCSpace, private val coroutineDispatcherProvider: CoroutinesDispatcherProvider @@ -52,6 +55,9 @@ class SpaceMembersViewModel( private val _spacePermissions = MutableStateFlow>>?>(null) val spacePermissions: StateFlow>>?> = _spacePermissions + private val _users = MutableStateFlow>>?>(null) + val users: StateFlow>>?> = _users + init { runUseCaseWithResult( coroutineDispatcher = coroutineDispatcherProvider.io, @@ -82,4 +88,17 @@ class SpaceMembersViewModel( requiresConnection = true ) + fun searchUsers(query: String) = runUseCaseWithResult( + coroutineDispatcher = coroutineDispatcherProvider.io, + flow = _users, + useCase = searchUsersUseCase, + useCaseParams = SearchUsersUseCase.Params(accountName = accountName, query = query), + showLoading = false, + requiresConnection = true + ) + + fun clearSearch() { + _users.value = Event(UIResult.Success(emptyList())) + } + } diff --git a/owncloudApp/src/main/res/layout/member_item.xml b/owncloudApp/src/main/res/layout/member_item.xml index ab5f52997ee..c23fa978ec3 100644 --- a/owncloudApp/src/main/res/layout/member_item.xml +++ b/owncloudApp/src/main/res/layout/member_item.xml @@ -1,7 +1,7 @@