Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/4974
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Remove a share over a file or a folder on an oCIS server

A new option to remove a share over a file or a folder on an oCIS has been added.
It will be only visible for users with proper permissions.

https://github.com/owncloud/android/issues/4940
https://github.com/owncloud/android/pull/4974
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUs
import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUseCase
import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase
import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.RemoveGraphShareAsyncUseCase
import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.DisableSpaceUseCase
import com.owncloud.android.domain.spaces.usecases.EditSpaceImageUseCase
Expand Down Expand Up @@ -241,6 +242,7 @@ val useCaseModule = module {
factoryOf(::GetShareesAsyncUseCase)
factoryOf(::GetSharesAsLiveDataUseCase)
factoryOf(::RefreshSharesFromServerAsyncUseCase)
factoryOf(::RemoveGraphShareAsyncUseCase)

// Spaces
factoryOf(::CreateSpaceUseCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package com.owncloud.android.presentation.sharing

import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
Expand All @@ -32,6 +33,8 @@ import com.owncloud.android.R
import com.owncloud.android.databinding.MembersFragmentBinding
import com.owncloud.android.domain.files.model.OCFile
import com.owncloud.android.domain.roles.model.OCRole
import com.owncloud.android.domain.sharing.shares.model.MemberPermission
import com.owncloud.android.extensions.avoidScreenshotsIfNeeded
import com.owncloud.android.extensions.collectLatestLifecycleFlow
import com.owncloud.android.extensions.showErrorInSnackbar
import com.owncloud.android.extensions.showMessageInSnackbar
Expand All @@ -40,7 +43,7 @@ import org.koin.androidx.viewmodel.ext.android.activityViewModel
import org.koin.core.parameter.parametersOf
import timber.log.Timber

class GraphShareFragment : Fragment() {
class GraphShareFragment : Fragment(), GraphSharesAdapter.GraphSharesAdapterListener {
private var _binding: MembersFragmentBinding? = null
private val binding get() = _binding!!

Expand All @@ -55,6 +58,7 @@ class GraphShareFragment : Fragment() {

private var roles: List<OCRole> = emptyList()
private var listener: GraphShareFragmentListener? = null
private var canRemoveShares: Boolean = false

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = MembersFragmentBinding.inflate(inflater, container, false)
Expand All @@ -65,7 +69,7 @@ class GraphShareFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
binding.membersTitle.text = getString(R.string.share_with_people_title)

graphSharesAdapter = GraphSharesAdapter()
graphSharesAdapter = GraphSharesAdapter(this)
binding.membersRecyclerView.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = graphSharesAdapter
Expand Down Expand Up @@ -104,10 +108,21 @@ class GraphShareFragment : Fragment() {
_binding = null
}

override fun onRemoveShare(share: MemberPermission) {
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.confirmation_remove_share_message, share.displayName))
.setPositiveButton(getString(R.string.common_yes)) { _, _ -> graphShareViewModel.removeGraphShare(share.id) }
.setNegativeButton(getString(R.string.common_no)) { dialog, _ -> dialog.dismiss() }
.show()
.avoidScreenshotsIfNeeded()
}

private fun subscribeToViewModels() {
observeRoles()
observeShares()
observeSpacePermissions()
observeAddShareResult()
observeRemoveShareResult()
}

private fun observeRoles() {
Expand Down Expand Up @@ -139,7 +154,7 @@ class GraphShareFragment : Fragment() {
val hasMembers = it.members.isNotEmpty()
binding.membersRecyclerView.isVisible = hasMembers
binding.noSharesMessage.isVisible = !hasMembers
graphSharesAdapter.setShares(it.members, it.roles)
graphSharesAdapter.setShares(it.members, it.roles, canRemoveShares)
binding.swipeRefreshMembers.isRefreshing = false
}
}
Expand All @@ -154,6 +169,28 @@ class GraphShareFragment : Fragment() {
}
}

private fun observeSpacePermissions() {
collectLatestLifecycleFlow(graphShareViewModel.spacePermissions) { event ->
event?.let {
when (val uiResult = event.peekContent()) {
is UIResult.Success -> {
uiResult.data?.let { spacePermissions ->
checkPermissions(spacePermissions)
}
}
is UIResult.Loading -> { }
is UIResult.Error -> {
Timber.e(uiResult.error, "Failed to retrieve space permissions")
}
}
}
}
}

private fun checkPermissions(spacePermissions: List<String>) {
canRemoveShares = DRIVES_DELETE_PERMISSION in spacePermissions
}

private fun observeAddShareResult() {
collectLatestLifecycleFlow(graphShareViewModel.addShareResultFlow) { event ->
event?.peekContent()?.let { uiResult ->
Expand All @@ -169,13 +206,30 @@ class GraphShareFragment : Fragment() {
}
}

private fun observeRemoveShareResult() {
collectLatestLifecycleFlow(graphShareViewModel.removeShareResultFlow) { uiResult ->
when (uiResult) {
is UIResult.Loading -> { }
is UIResult.Success -> {
showMessageInSnackbar(getString(R.string.share_remove_correctly))
graphShareViewModel.getGraphShares()
}
is UIResult.Error -> {
showErrorInSnackbar(R.string.share_remove_failed, uiResult.error)
Timber.e(uiResult.error, "Failed to remove a graph share")
}
}
}
}

interface GraphShareFragmentListener {
fun addGraphShare(file: OCFile, accountName: String)
}

companion object {
private const val ARG_FILE = "FILE"
private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME"
private const val DRIVES_DELETE_PERMISSION = "libre.graph/driveItem/permissions/delete"

fun newInstance(file: OCFile, accountName: String): GraphShareFragment {
val args = Bundle().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import com.owncloud.android.domain.roles.model.OCRole
import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.AddGraphShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.RemoveGraphShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.model.OCPermissions
import com.owncloud.android.domain.user.usecases.GetUserIdAsyncUseCase
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
Expand All @@ -55,6 +57,8 @@ class GraphShareViewModel(
private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase,
private val searchMembersUseCase: SearchMembersUseCase,
private val getUserIdAsyncUseCase: GetUserIdAsyncUseCase,
private val getSpacePermissionsAsyncUseCase: GetSpacePermissionsAsyncUseCase,
private val removeGraphShareAsyncUseCase: RemoveGraphShareAsyncUseCase,
private val accountName: String,
private val file: OCFile,
private val coroutineDispatcherProvider: CoroutinesDispatcherProvider,
Expand All @@ -78,9 +82,15 @@ class GraphShareViewModel(
private val _addShareResultFlow = MutableStateFlow<Event<UIResult<Unit>>?>(null)
val addShareResultFlow: StateFlow<Event<UIResult<Unit>>?> = _addShareResultFlow

private val _removeShareResultFlow = MutableSharedFlow<UIResult<Unit>>()
val removeShareResultFlow: SharedFlow<UIResult<Unit>> = _removeShareResultFlow

private var searchJob: Job? = null
var capabilities: OCCapability? = null

private val _spacePermissions = MutableStateFlow<Event<UIResult<List<String>>>?>(null)
val spacePermissions: StateFlow<Event<UIResult<List<String>>>?> = _spacePermissions

init {
runUseCaseWithResult(
coroutineDispatcher = coroutineDispatcherProvider.io,
Expand All @@ -98,6 +108,24 @@ class GraphShareViewModel(
viewModelScope.launch(coroutineDispatcherProvider.io) {
capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))
}
getSpacePermissions()
}

fun getSpacePermissions() {
val spaceId = file.spaceId
if (spaceId == null) {
_spacePermissions.update { Event(UIResult.Error(error = IncompleteFileDataException())) }
return
}

runUseCaseWithResult(
coroutineDispatcher = coroutineDispatcherProvider.io,
flow = _spacePermissions,
useCase = getSpacePermissionsAsyncUseCase,
useCaseParams = GetSpacePermissionsAsyncUseCase.Params(accountName = accountName, spaceId = spaceId),
showLoading = false,
requiresConnection = true
)
}

fun getGraphShares() {
Expand Down Expand Up @@ -144,6 +172,29 @@ class GraphShareViewModel(
)
}

fun removeGraphShare(shareId: String) {
val spaceId = file.spaceId
val itemId = file.remoteId
if (spaceId == null || itemId == null) {
viewModelScope.launch(coroutineDispatcherProvider.io) {
_removeShareResultFlow.emit(UIResult.Error(error = IncompleteFileDataException()))
}
return
}

runUseCaseWithResult(
coroutineDispatcher = coroutineDispatcherProvider.io,
sharedFlow = _removeShareResultFlow,
useCase = removeGraphShareAsyncUseCase,
useCaseParams = RemoveGraphShareAsyncUseCase.Params(
accountName = accountName,
spaceId = spaceId,
itemId = itemId,
shareId = shareId,
)
)
}

fun searchMembers(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch(coroutineDispatcherProvider.io) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ import com.owncloud.android.domain.sharing.shares.model.MemberPermission
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.PreferenceUtils

class GraphSharesAdapter : RecyclerView.Adapter<GraphSharesAdapter.GraphShareViewHolder>() {
class GraphSharesAdapter(
private val listener: GraphSharesAdapterListener,
) : RecyclerView.Adapter<GraphSharesAdapter.GraphShareViewHolder>() {

private var shares: List<MemberPermission> = emptyList()
private var rolesMap: Map<String, String> = emptyMap()
private var canRemoveShares = false

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphShareViewHolder {
val inflater = LayoutInflater.from(parent.context)
Expand All @@ -58,6 +61,14 @@ class GraphSharesAdapter : RecyclerView.Adapter<GraphSharesAdapter.GraphShareVie
)
memberRole.text = roleNames.joinToString(", ")

removeMemberButton.apply {
contentDescription = holder.itemView.context.getString(R.string.content_description_delete_share, share.displayName)
isVisible = canRemoveShares
setOnClickListener {
listener.onRemoveShare(share)
}
}

val hasExpirationDate = share.expirationDateTime != null
expirationCalendarIcon.isVisible = hasExpirationDate
expirationDate.isVisible = hasExpirationDate
Expand All @@ -71,18 +82,24 @@ class GraphSharesAdapter : RecyclerView.Adapter<GraphSharesAdapter.GraphShareVie

override fun getItemCount(): Int = shares.size

fun setShares(shares: List<MemberPermission>, roles: List<OCRole>) {
fun setShares(shares: List<MemberPermission>, roles: List<OCRole>, canRemoveShares: Boolean) {
val hasUserPermissionsChanged = this.canRemoveShares != canRemoveShares
this.canRemoveShares = canRemoveShares
this.rolesMap = roles.associate { it.id to it.displayName }
val sortedShares = shares.sortedWith(
compareBy<MemberPermission> { it.isGroup }
.thenBy { it.displayName.lowercase() }
)
val diffResult = DiffUtil.calculateDiff(GraphSharesDiffUtil(this.shares, sortedShares))
val diffResult = DiffUtil.calculateDiff(GraphSharesDiffUtil(this.shares, sortedShares, hasUserPermissionsChanged))
this.shares = sortedShares
diffResult.dispatchUpdatesTo(this)
}

class GraphShareViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val binding = MemberItemBinding.bind(itemView)
}

interface GraphSharesAdapterListener {
fun onRemoveShare(share: MemberPermission)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.owncloud.android.domain.sharing.shares.model.MemberPermission
class GraphSharesDiffUtil(
private val oldList: List<MemberPermission>,
private val newList: List<MemberPermission>,
private val hasUserPermissionsChanged: Boolean = false,
) : DiffUtil.Callback() {

override fun getOldListSize(): Int = oldList.size
Expand All @@ -36,5 +37,5 @@ class GraphSharesDiffUtil(
oldList[oldItemPosition].id == newList[newItemPosition].id

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
oldList[oldItemPosition] == newList[newItemPosition] && !hasUserPermissionsChanged
}
2 changes: 2 additions & 0 deletions owncloudApp/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@
<string name="share_add_correctly">Share created correctly</string>
<string name="share_add_failed">Share could not be created</string>
<string name="share_add_conflict_error">Is already shared with this user/group</string>
<string name="share_remove_correctly">Share removed correctly</string>
<string name="share_remove_failed">Share could not be removed</string>
<string name="share_via_link_section_title">Public links</string>
<string name="share_via_link_create_title">Create link share</string>
<string name="share_via_link_edit_title">Edit link share</string>
Expand Down
Loading