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..ae73313bf84 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.SearchMembersUseCase
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(::SearchMembersUseCase)
}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt
index 99dc525a20f..4c65b00650d 100644
--- a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt
@@ -31,7 +31,7 @@ fun SpaceMenuOption.toStringResId() =
SpaceMenuOption.ENABLE -> R.string.enable_space
SpaceMenuOption.DELETE -> R.string.delete_space
SpaceMenuOption.SET_ICON -> R.string.set_space_icon
- SpaceMenuOption.MEMBERS -> R.string.space_members
+ SpaceMenuOption.MEMBERS -> R.string.members_title
}
fun SpaceMenuOption.toDrawableResId() =
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..28faaf41113
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/AddMemberFragment.kt
@@ -0,0 +1,143 @@
+/**
+ * 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.core.view.isVisible
+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.spaces.model.OCSpace
+import com.owncloud.android.domain.spaces.model.SpaceMember
+import com.owncloud.android.extensions.collectLatestLifecycleFlow
+import com.owncloud.android.extensions.showErrorInSnackbar
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+import timber.log.Timber
+
+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
+
+ 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)
+ searchMembersAdapter = SearchMembersAdapter()
+ recyclerView = binding.membersRecyclerView
+ recyclerView.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ adapter = searchMembersAdapter
+ }
+
+ val spaceMembers = requireArguments().getParcelableArrayList(ARG_SPACE_MEMBERS) ?: arrayListOf()
+
+ collectLatestLifecycleFlow(spaceMembersViewModel.members) { uiState ->
+ if (uiState.isLoading) {
+ binding.indeterminateProgressBar.visibility = View.VISIBLE
+ binding.emptyDataParent.root.visibility = View.GONE
+ binding.membersRecyclerView.visibility = View.GONE
+ } else {
+ binding.indeterminateProgressBar.visibility = View.GONE
+ val listOfMembersFiltered = uiState.members.filter { member ->
+ !spaceMembers.any { spaceMember ->
+ spaceMember.id == "u:${member.id}" || spaceMember.id == "g:${member.id}" }
+ }
+ val hasMembers = listOfMembersFiltered.isNotEmpty()
+ showOrHideEmptyView(hasMembers)
+ if (hasMembers) searchMembersAdapter.setMembers(listOfMembersFiltered)
+ uiState.error?.let {
+ Timber.e(uiState.error, "Failed to retrieve available users and groups")
+ showErrorInSnackbar(R.string.members_search_failed, uiState.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.searchMembers(newText) } else { spaceMembersViewModel.clearSearch() }
+ return true
+ }
+ })
+ }
+ }
+
+ private fun showOrHideEmptyView(hasMembers: Boolean) {
+ binding.membersRecyclerView.isVisible = hasMembers
+ binding.emptyDataParent.apply {
+ val shouldShow = !hasMembers && binding.searchBar.query.length > 2
+ root.isVisible = shouldShow
+ if (shouldShow) {
+ listEmptyDatasetIcon.setImageResource(R.drawable.ic_share_generic_white)
+ listEmptyDatasetTitle.setText(R.string.members_search_failed)
+ listEmptyDatasetSubTitle.setText(R.string.members_search_empty)
+ }
+ }
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ requireActivity().setTitle(R.string.members_add)
+ }
+
+ companion object {
+ private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME"
+ private const val ARG_CURRENT_SPACE = "CURRENT_SPACE"
+ private const val ARG_SPACE_MEMBERS = "SPACE_MEMBERS"
+
+ fun newInstance(
+ accountName: String,
+ currentSpace: OCSpace,
+ spaceMembers: List
+ ): AddMemberFragment {
+ val args = Bundle().apply {
+ putString(ARG_ACCOUNT_NAME, accountName)
+ putParcelable(ARG_CURRENT_SPACE, currentSpace)
+ putParcelableArrayList(ARG_SPACE_MEMBERS, ArrayList(spaceMembers))
+ }
+ 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..5ba99307f46
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SearchMembersAdapter.kt
@@ -0,0 +1,82 @@
+/**
+ * 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.DiffUtil
+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 = mutableListOf()
+
+ 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 {
+ val isGroup = member.surname == GROUP_SURNAME
+ memberIcon.setImageResource(if (isGroup) R.drawable.ic_group else R.drawable.ic_user)
+ memberName.text = member.displayName
+ memberName.contentDescription = holder.itemView.context.getString(
+ if (isGroup) R.string.content_description_member_group else R.string.content_description_member_user, member.displayName
+ )
+ memberRole.text = if (isGroup) {
+ holder.itemView.context.getString(R.string.member_type_group)
+ } else {
+ if (member.surname == USER_SURNAME) holder.itemView.context.getString(R.string.member_type_user) else member.surname
+ }
+ }
+ }
+
+ override fun getItemCount(): Int = members.size
+
+ fun setMembers(members: List) {
+ val diffCallback = SpaceMembersDiffUtil(this.members, members)
+ val diffResult = DiffUtil.calculateDiff(diffCallback)
+ this.members.clear()
+ this.members.addAll(members)
+ diffResult.dispatchUpdatesTo(this)
+ }
+
+ class SearchMembersViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val binding = MemberItemBinding.bind(itemView)
+ }
+
+ companion object {
+ private const val USER_SURNAME = "User"
+ private const val GROUP_SURNAME = "Group"
+ }
+}
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 1d96476ef63..4c47841792b 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
@@ -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,
@@ -25,21 +25,43 @@ import android.view.Menu
import android.view.MenuItem
import androidx.fragment.app.transaction
import com.owncloud.android.R
+import com.owncloud.android.databinding.MembersActivityBinding
import com.owncloud.android.domain.spaces.model.OCSpace
+import com.owncloud.android.domain.spaces.model.SpaceMember
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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.members_activity)
+ binding = MembersActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
setupStandardToolbar(title = null, displayHomeAsUpEnabled = true, homeButtonEnabled = true, displayShowTitleEnabled = true)
supportActionBar?.setHomeActionContentDescription(R.string.common_back)
- val currentSpace = intent.getParcelableExtra(EXTRA_SPACE)
+ val currentSpace = intent.getParcelableExtra(EXTRA_SPACE) ?: return
+ binding.apply {
+ itemName.text = currentSpace.name
+ currentSpace.quota?.let { quota ->
+ val usedQuota = quota.used
+ val totalQuota = quota.total
+ itemSize.text = when {
+ usedQuota == null -> getString(R.string.drawer_unavailable_used_storage)
+ totalQuota == 0L -> DisplayUtils.bytesToHumanReadable(usedQuota, baseContext, true)
+ else -> getString(
+ R.string.drawer_quota,
+ DisplayUtils.bytesToHumanReadable(usedQuota, baseContext, true),
+ DisplayUtils.bytesToHumanReadable(totalQuota, baseContext, true),
+ quota.getRelative().toString())
+ }
+ }
+ }
supportFragmentManager.transaction {
if (savedInstanceState == null && currentSpace != null) {
@@ -59,8 +81,19 @@ class SpaceMembersActivity: FileActivity() {
super.onOptionsItemSelected(item)
}
+ override fun addMember(space: OCSpace, spaceMembers: List) {
+ val addMemberFragment = AddMemberFragment.newInstance(account.name, space, spaceMembers)
+ val transaction = supportFragmentManager.beginTransaction()
+ transaction.apply {
+ replace(R.id.members_fragment_container, addMemberFragment, TAG_ADD_MEMBER_FRAGMENT)
+ addToBackStack(null)
+ commit()
+ }
+ }
+
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/SpaceMembersAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersAdapter.kt
index f33a0bf9283..d96182f3c9f 100644
--- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersAdapter.kt
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersAdapter.kt
@@ -73,7 +73,9 @@ class SpaceMembersAdapter: RecyclerView.Adapter) {
this.rolesMap = roles.associate { it.id to it.displayName }
- this.members = spaceMembers.members.sortedByDescending { member -> roles.indexOfFirst { it.id in member.roles } }
+ this.members = spaceMembers.members.sortedWith(compareByDescending {
+ member -> roles.indexOfFirst { it.id in member.roles } }.thenBy { member -> member.displayName }
+ )
notifyDataSetChanged()
}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersDiffUtil.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersDiffUtil.kt
new file mode 100644
index 00000000000..f200eacd186
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersDiffUtil.kt
@@ -0,0 +1,47 @@
+/**
+ * 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 androidx.recyclerview.widget.DiffUtil
+import com.owncloud.android.domain.members.model.OCMember
+
+class SpaceMembersDiffUtil(
+ private val oldList: List,
+ private val newList: List
+) : DiffUtil.Callback() {
+ override fun getOldListSize(): Int = oldList.size
+
+ override fun getNewListSize(): Int = newList.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val oldItem = oldList[oldItemPosition]
+ val newItem = newList[newItemPosition]
+
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val oldItem = oldList[oldItemPosition]
+ val newItem = newList[newItemPosition]
+
+ return ((oldItem.id == newItem.id) && (oldItem.displayName == newItem.displayName) && (oldItem.surname == newItem.surname))
+ }
+}
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..c5f1e00c35c 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,
@@ -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
@@ -31,11 +32,12 @@ 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
+import com.owncloud.android.domain.spaces.model.SpaceMember
import com.owncloud.android.extensions.collectLatestLifecycleFlow
import com.owncloud.android.presentation.common.UIResult
-import com.owncloud.android.utils.DisplayUtils
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
@@ -52,6 +54,8 @@ class SpaceMembersFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private var roles: List = emptyList()
+ private var spaceMembers: List = emptyList()
+ private var listener: SpaceMemberFragmentListener? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = MembersFragmentBinding.inflate(inflater, container, false)
@@ -67,6 +71,8 @@ class SpaceMembersFragment : Fragment() {
adapter = spaceMembersAdapter
}
+ val currentSpace = requireArguments().getParcelable(ARG_CURRENT_SPACE) ?: return
+
collectLatestLifecycleFlow(spaceMembersViewModel.roles) { event ->
event?.let {
when (val uiResult = event.peekContent()) {
@@ -77,7 +83,9 @@ class SpaceMembersFragment : Fragment() {
}
}
is UIResult.Loading -> { }
- is UIResult.Error -> { }
+ is UIResult.Error -> {
+ Timber.e(uiResult.error, "Failed to retrieve platform roles")
+ }
}
}
}
@@ -87,37 +95,64 @@ class SpaceMembersFragment : Fragment() {
when (val uiResult = event.peekContent()) {
is UIResult.Success -> {
uiResult.data?.let {
- if (roles.isNotEmpty()) { spaceMembersAdapter.setSpaceMembers(it, roles) }
+ if (roles.isNotEmpty()) {
+ spaceMembersAdapter.setSpaceMembers(it, roles)
+ spaceMembers = it.members
+ }
}
}
is UIResult.Loading -> { }
- is UIResult.Error -> { }
+ is UIResult.Error -> {
+ Timber.e(uiResult.error, "Failed to retrieve space members for space: ${currentSpace.id} (${currentSpace?.id})")
+ }
}
}
}
- val currentSpace = requireArguments().getParcelable(ARG_CURRENT_SPACE) ?: return
- binding.apply {
- itemName.text = currentSpace.name
- currentSpace.quota?.let { quota ->
- val usedQuota = quota.used
- val totalQuota = quota.total
- itemSize.text = when {
- usedQuota == null -> getString(R.string.drawer_unavailable_used_storage)
- totalQuota == 0L -> DisplayUtils.bytesToHumanReadable(usedQuota, requireContext(), true)
- else -> getString(
- R.string.drawer_quota,
- DisplayUtils.bytesToHumanReadable(usedQuota, requireContext(), true),
- DisplayUtils.bytesToHumanReadable(totalQuota, requireContext(), true),
- quota.getRelative().toString())
+ 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 -> {
+ Timber.e(uiResult.error, "Failed to retrieve space permissions for space: ${currentSpace.id} (${currentSpace?.id})")
+ }
}
}
}
+
+ binding.addMemberButton.setOnClickListener {
+ listener?.addMember(currentSpace, spaceMembers)
+ }
+ }
+
+ 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, spaceMembers: List)
}
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..632f11e7f23 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,
@@ -21,21 +21,32 @@
package com.owncloud.android.presentation.spaces.members
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.owncloud.android.domain.UseCaseResult
+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.SearchMembersUseCase
import com.owncloud.android.domain.utils.Event
import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult
import com.owncloud.android.presentation.common.UIResult
import com.owncloud.android.providers.CoroutinesDispatcherProvider
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
class SpaceMembersViewModel(
private val getRolesAsyncUseCase: GetRolesAsyncUseCase,
private val getSpaceMembersUseCase: GetSpaceMembersUseCase,
+ private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase,
+ private val searchMembersUseCase: SearchMembersUseCase,
private val accountName: String,
private val space: OCSpace,
private val coroutineDispatcherProvider: CoroutinesDispatcherProvider
@@ -47,6 +58,14 @@ class SpaceMembersViewModel(
private val _spaceMembers = MutableStateFlow>?>(null)
val spaceMembers: StateFlow>?> = _spaceMembers
+ private val _spacePermissions = MutableStateFlow>>?>(null)
+ val spacePermissions: StateFlow>>?> = _spacePermissions
+
+ private val _members: MutableSharedFlow = MutableSharedFlow()
+ val members: SharedFlow = _members
+
+ private var searchJob: Job? = null
+
init {
runUseCaseWithResult(
coroutineDispatcher = coroutineDispatcherProvider.io,
@@ -56,6 +75,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(
@@ -67,4 +96,26 @@ class SpaceMembersViewModel(
requiresConnection = true
)
+ fun searchMembers(query: String) {
+ searchJob?.cancel()
+ searchJob = viewModelScope.launch(coroutineDispatcherProvider.io) {
+ _members.emit(MembersUIState(members = emptyList(), isLoading = true , error = null))
+ when (val result = searchMembersUseCase(SearchMembersUseCase.Params(accountName, query))) {
+ is UseCaseResult.Success -> _members.emit(MembersUIState(members = result.data, isLoading = false, error = null))
+ is UseCaseResult.Error -> _members.emit(MembersUIState(members = emptyList(), isLoading = false, error = result.getThrowableOrNull()))
+ }
+ }
+ }
+
+ fun clearSearch() {
+ viewModelScope.launch(coroutineDispatcherProvider.io) {
+ _members.emit(MembersUIState(members = emptyList(), isLoading = false , error = null))
+ }
+ }
+
+ data class MembersUIState (
+ val members: List,
+ val isLoading: Boolean,
+ val error: Throwable?
+ )
}
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..c03bd6e40b4
--- /dev/null
+++ b/owncloudApp/src/main/res/layout/add_member_fragment.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
+
+
+
+
+
+
+
+
+
+