Skip to content

Commit 6f332ff

Browse files
committed
feat: add member search screen for creating oCIS shares
Signed-off-by: Jorge Aguado Recio <jaguado@izertis.com>
1 parent 89507e2 commit 6f332ff

3 files changed

Lines changed: 211 additions & 2 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* ownCloud Android client application
3+
*
4+
* @author Jorge Aguado Recio
5+
*
6+
* Copyright (C) 2026 ownCloud GmbH.
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License version 2,
10+
* as published by the Free Software Foundation.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.owncloud.android.presentation.sharing
22+
23+
import android.os.Bundle
24+
import android.view.LayoutInflater
25+
import android.view.View
26+
import android.view.ViewGroup
27+
import androidx.appcompat.widget.SearchView
28+
import androidx.core.view.isVisible
29+
import androidx.fragment.app.Fragment
30+
import androidx.recyclerview.widget.LinearLayoutManager
31+
import androidx.recyclerview.widget.RecyclerView
32+
import com.owncloud.android.R
33+
import com.owncloud.android.databinding.AddMemberFragmentBinding
34+
import com.owncloud.android.domain.files.model.OCFile
35+
import com.owncloud.android.domain.members.model.OCMember
36+
import com.owncloud.android.extensions.collectLatestLifecycleFlow
37+
import com.owncloud.android.extensions.showErrorInSnackbar
38+
import com.owncloud.android.presentation.spaces.members.SearchMembersAdapter
39+
import org.koin.androidx.viewmodel.ext.android.activityViewModel
40+
import org.koin.core.parameter.parametersOf
41+
import timber.log.Timber
42+
43+
class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdapterListener {
44+
private var _binding: AddMemberFragmentBinding? = null
45+
private val binding get() = _binding!!
46+
47+
private val graphShareViewModel by activityViewModel<GraphShareViewModel> {
48+
parametersOf(
49+
requireArguments().getString(ARG_ACCOUNT_NAME),
50+
requireArguments().getParcelable<OCFile>(ARG_FILE)
51+
)
52+
}
53+
54+
private lateinit var searchMembersAdapter: SearchMembersAdapter
55+
private lateinit var recyclerView: RecyclerView
56+
57+
private var searchMinLength = DEFAULT_SEARCH_MIN_LENGTH
58+
59+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
60+
_binding = AddMemberFragmentBinding.inflate(inflater, container, false)
61+
return binding.root
62+
}
63+
64+
override fun onDestroyView() {
65+
super.onDestroyView()
66+
_binding = null
67+
}
68+
69+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
70+
super.onViewCreated(view, savedInstanceState)
71+
searchMembersAdapter = SearchMembersAdapter(this)
72+
recyclerView = binding.membersRecyclerView
73+
recyclerView.apply {
74+
layoutManager = LinearLayoutManager(requireContext())
75+
adapter = searchMembersAdapter
76+
}
77+
78+
subscribeToViewModels()
79+
80+
binding.searchBar.apply {
81+
if (savedInstanceState == null) { requestFocus() }
82+
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
83+
override fun onQueryTextSubmit(query: String): Boolean = true
84+
85+
override fun onQueryTextChange(newText: String): Boolean {
86+
if (newText.length >= searchMinLength) {
87+
graphShareViewModel.searchMembers(newText)
88+
} else {
89+
graphShareViewModel.clearSearch()
90+
}
91+
return true
92+
}
93+
})
94+
}
95+
}
96+
97+
override fun onMemberClick(member: OCMember) {
98+
99+
}
100+
101+
private fun showOrHideEmptyView(hasMembers: Boolean) {
102+
binding.membersRecyclerView.isVisible = hasMembers
103+
binding.emptyDataParent.apply {
104+
val shouldShow = !hasMembers && binding.searchBar.query.length >= searchMinLength
105+
root.isVisible = shouldShow
106+
if (shouldShow) {
107+
listEmptyDatasetIcon.setImageResource(R.drawable.ic_share_generic_white)
108+
listEmptyDatasetTitle.setText(R.string.members_search_failed)
109+
listEmptyDatasetSubTitle.setText(R.string.members_search_empty)
110+
}
111+
}
112+
}
113+
114+
private fun subscribeToViewModels() {
115+
searchMinLength = graphShareViewModel.capabilities?.filesSharingSearchMinLength ?: DEFAULT_SEARCH_MIN_LENGTH
116+
117+
collectLatestLifecycleFlow(graphShareViewModel.members) { uiState ->
118+
if (uiState.isLoading) {
119+
binding.indeterminateProgressBar.visibility = View.VISIBLE
120+
binding.emptyDataParent.root.visibility = View.GONE
121+
binding.membersRecyclerView.visibility = View.GONE
122+
} else {
123+
binding.indeterminateProgressBar.visibility = View.GONE
124+
val hasMembers = uiState.members.isNotEmpty()
125+
showOrHideEmptyView(hasMembers)
126+
if (hasMembers) searchMembersAdapter.setMembers(uiState.members)
127+
uiState.error?.let {
128+
Timber.e(uiState.error, "Failed to retrieve available users and groups")
129+
showErrorInSnackbar(R.string.members_search_failed, uiState.error)
130+
}
131+
}
132+
}
133+
}
134+
135+
companion object {
136+
private const val ARG_FILE = "FILE"
137+
private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME"
138+
private const val DEFAULT_SEARCH_MIN_LENGTH = 3
139+
140+
fun newInstance(file: OCFile, accountName: String): AddGraphShareFragment {
141+
val args = Bundle().apply {
142+
putParcelable(ARG_FILE, file)
143+
putString(ARG_ACCOUNT_NAME, accountName)
144+
}
145+
return AddGraphShareFragment().apply {
146+
arguments = args
147+
}
148+
}
149+
}
150+
}

‎owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareFragment.kt‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ import com.owncloud.android.domain.roles.model.OCRole
3434
import com.owncloud.android.extensions.collectLatestLifecycleFlow
3535
import com.owncloud.android.extensions.showErrorInSnackbar
3636
import com.owncloud.android.presentation.common.UIResult
37-
import org.koin.androidx.viewmodel.ext.android.viewModel
37+
import org.koin.androidx.viewmodel.ext.android.activityViewModel
3838
import org.koin.core.parameter.parametersOf
3939
import timber.log.Timber
4040

4141
class GraphShareFragment : Fragment() {
4242
private var _binding: MembersFragmentBinding? = null
4343
private val binding get() = _binding!!
4444

45-
private val graphShareViewModel by viewModel<GraphShareViewModel> {
45+
private val graphShareViewModel by activityViewModel<GraphShareViewModel> {
4646
parametersOf(
4747
requireArguments().getString(ARG_ACCOUNT_NAME),
4848
requireArguments().getParcelable<OCFile>(ARG_FILE)
@@ -73,7 +73,21 @@ class GraphShareFragment : Fragment() {
7373
}
7474

7575
val file = requireArguments().getParcelable<OCFile>(ARG_FILE)
76+
val accountName = requireArguments().getString(ARG_ACCOUNT_NAME)
7677
binding.addMemberButton.isVisible = file?.hasResharePermission ?: false
78+
binding.addMemberButton.setOnClickListener {
79+
if (file != null && accountName != null) {
80+
parentFragmentManager.beginTransaction().apply {
81+
replace(
82+
R.id.members_fragment_container,
83+
AddGraphShareFragment.newInstance(file, accountName),
84+
TAG_ADD_GRAPH_SHARE_FRAGMENT
85+
)
86+
addToBackStack(null)
87+
commit()
88+
}
89+
}
90+
}
7791

7892
subscribeToViewModels()
7993
}
@@ -135,6 +149,7 @@ class GraphShareFragment : Fragment() {
135149
companion object {
136150
private const val ARG_FILE = "FILE"
137151
private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME"
152+
private const val TAG_ADD_GRAPH_SHARE_FRAGMENT = "ADD_GRAPH_SHARE_FRAGMENT"
138153

139154
fun newInstance(file: OCFile, accountName: String): GraphShareFragment {
140155
val args = Bundle().apply {

‎owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/GraphShareViewModel.kt‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@
2121
package com.owncloud.android.presentation.sharing
2222

2323
import androidx.lifecycle.ViewModel
24+
import androidx.lifecycle.viewModelScope
25+
import com.owncloud.android.domain.UseCaseResult
26+
import com.owncloud.android.domain.capabilities.model.OCCapability
27+
import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
2428
import com.owncloud.android.domain.exceptions.IncompleteFileDataException
2529
import com.owncloud.android.domain.files.model.OCFile
30+
import com.owncloud.android.domain.members.model.OCMember
31+
import com.owncloud.android.domain.members.usecases.SearchMembersUseCase
2632
import com.owncloud.android.domain.roles.model.OCRole
2733
import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase
2834
import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUseCase
@@ -31,13 +37,19 @@ import com.owncloud.android.domain.utils.Event
3137
import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult
3238
import com.owncloud.android.presentation.common.UIResult
3339
import com.owncloud.android.providers.CoroutinesDispatcherProvider
40+
import kotlinx.coroutines.Job
41+
import kotlinx.coroutines.flow.MutableSharedFlow
3442
import kotlinx.coroutines.flow.MutableStateFlow
43+
import kotlinx.coroutines.flow.SharedFlow
3544
import kotlinx.coroutines.flow.StateFlow
3645
import kotlinx.coroutines.flow.update
46+
import kotlinx.coroutines.launch
3747

3848
class GraphShareViewModel(
3949
private val getRolesAsyncUseCase: GetRolesAsyncUseCase,
4050
private val getGraphSharesAsyncUseCase: GetGraphSharesAsyncUseCase,
51+
private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase,
52+
private val searchMembersUseCase: SearchMembersUseCase,
4153
private val accountName: String,
4254
private val file: OCFile,
4355
private val coroutineDispatcherProvider: CoroutinesDispatcherProvider,
@@ -49,13 +61,22 @@ class GraphShareViewModel(
4961
private val _shares = MutableStateFlow<Event<UIResult<OCPermissions>>?>(null)
5062
val shares: StateFlow<Event<UIResult<OCPermissions>>?> = _shares
5163

64+
private val _members: MutableSharedFlow<MembersUIState> = MutableSharedFlow()
65+
val members: SharedFlow<MembersUIState> = _members
66+
67+
private var searchJob: Job? = null
68+
var capabilities: OCCapability? = null
69+
5270
init {
5371
runUseCaseWithResult(
5472
coroutineDispatcher = coroutineDispatcherProvider.io,
5573
flow = _roles,
5674
useCase = getRolesAsyncUseCase,
5775
useCaseParams = GetRolesAsyncUseCase.Params(accountName = accountName),
5876
)
77+
viewModelScope.launch(coroutineDispatcherProvider.io) {
78+
capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))
79+
}
5980
}
6081

6182
fun getGraphShares() {
@@ -78,4 +99,27 @@ class GraphShareViewModel(
7899
)
79100
)
80101
}
102+
103+
fun searchMembers(query: String) {
104+
searchJob?.cancel()
105+
searchJob = viewModelScope.launch(coroutineDispatcherProvider.io) {
106+
_members.emit(MembersUIState(members = emptyList(), isLoading = true, error = null))
107+
when (val result = searchMembersUseCase(SearchMembersUseCase.Params(accountName, query))) {
108+
is UseCaseResult.Success -> _members.emit(MembersUIState(members = result.data, isLoading = false, error = null))
109+
is UseCaseResult.Error -> _members.emit(MembersUIState(members = emptyList(), isLoading = false, error = result.getThrowableOrNull()))
110+
}
111+
}
112+
}
113+
114+
fun clearSearch() {
115+
viewModelScope.launch(coroutineDispatcherProvider.io) {
116+
_members.emit(MembersUIState(members = emptyList(), isLoading = false, error = null))
117+
}
118+
}
119+
120+
data class MembersUIState(
121+
val members: List<OCMember>,
122+
val isLoading: Boolean,
123+
val error: Throwable?
124+
)
81125
}

0 commit comments

Comments
 (0)