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/4978
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Edit a share over a file or a folder on an oCIS server

A new option to edit 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/4937
https://github.com/owncloud/android/pull/4978
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.AddGraphShareAsyncUse
import com.owncloud.android.domain.sharing.shares.usecases.CreatePrivateShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.CreatePublicShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.DeleteShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.EditGraphShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.EditPrivateShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.EditPublicShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUseCase
Expand Down Expand Up @@ -234,6 +235,7 @@ val useCaseModule = module {
factoryOf(::CreatePrivateShareAsyncUseCase)
factoryOf(::CreatePublicShareAsyncUseCase)
factoryOf(::DeleteShareAsyncUseCase)
factoryOf(::EditGraphShareAsyncUseCase)
factoryOf(::EditPrivateShareAsyncUseCase)
factoryOf(::EditPublicShareAsyncUseCase)
factoryOf(::GetGraphSharesAsyncUseCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ private const val GROUP_PREFIX = "g:"
private const val USER_PREFIX = "u:"

fun MemberPermission.toOCMember(): OCMember {
val isGroup = id.startsWith(GROUP_PREFIX)
val type = if (isGroup) OCMemberType.GROUP else OCMemberType.USER
return OCMember(
id = id.removePrefix(if (isGroup) GROUP_PREFIX else USER_PREFIX),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.owncloud.android.extensions.collectLatestLifecycleFlow
import com.owncloud.android.extensions.openDatePickerDialog
import com.owncloud.android.extensions.showErrorInSnackbar
import com.owncloud.android.extensions.showOrHideEmptyView
import com.owncloud.android.extensions.toOCMember
import com.owncloud.android.presentation.spaces.members.SearchMembersAdapter
import com.owncloud.android.presentation.spaces.members.SpaceRolesAdapter
import com.owncloud.android.utils.DisplayUtils
Expand All @@ -68,6 +69,8 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap
private var currentShares: List<MemberPermission> = emptyList()
private var searchMinLength = DEFAULT_SEARCH_MIN_LENGTH
private var currentUserId: String? = null
private var editMode = false
private var selectedShareId = ""

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = AddMemberFragmentBinding.inflate(inflater, container, false)
Expand All @@ -81,6 +84,10 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

editMode = requireArguments().getBoolean(ARG_EDIT_MODE, false)
roles = requireArguments().getParcelableArrayList(ARG_ROLES) ?: emptyList()

searchMembersAdapter = SearchMembersAdapter(this)
recyclerView = binding.membersRecyclerView
recyclerView.apply {
Expand All @@ -98,6 +105,13 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap
}
rolesAdapter.setRoles(roles)

if (editMode) {
val selectedShare = requireArguments().getParcelable<MemberPermission>(ARG_SELECTED_SHARE)
selectedShare?.let {
bindEditMode(it, roles)
}
}

subscribeToViewModels()

binding.searchBar.apply {
Expand All @@ -124,6 +138,15 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap
private fun subscribeToViewModels() {
searchMinLength = graphShareViewModel.capabilities?.filesSharingSearchMinLength ?: DEFAULT_SEARCH_MIN_LENGTH

observeUserId()
observeShares()
observeMembers()
observeAddShareUIState()
observeAddShareResult()
observeEditShareResult()
}

private fun observeUserId() {
collectLatestLifecycleFlow(graphShareViewModel.userId) { event ->
event?.let {
when (val uiResult = event.peekContent()) {
Expand All @@ -137,23 +160,23 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap
}
}
}
}

private fun observeShares() {
collectLatestLifecycleFlow(graphShareViewModel.shares) { event ->
event?.let {
when (val uiResult = event.peekContent()) {
is UIResult.Success -> {
uiResult.data?.let {
roles = it.roles
currentShares = it.members
rolesAdapter.setRoles(roles)
}
uiResult.data?.let { currentShares = it.members }
}
is UIResult.Loading -> { }
is UIResult.Error -> { Timber.e(uiResult.error, "Failed to retrieve shares") }
}
}
}
}

private fun observeMembers() {
collectLatestLifecycleFlow(graphShareViewModel.members) { uiState ->
if (uiState.isLoading) {
binding.indeterminateProgressBar.visibility = View.VISIBLE
Expand All @@ -174,14 +197,16 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap
}
}
}
}

private fun observeAddShareUIState() {
collectLatestLifecycleFlow(graphShareViewModel.addShareUIState) { uiState ->
uiState?.let {
binding.apply {
searchMemberLayout.visibility = View.GONE
addMemberLayout.visibility = View.VISIBLE
inviteMemberButton.visibility = View.VISIBLE
inviteMemberButton.text = getString(R.string.action_share)
inviteMemberButton.text = getString(if (editMode) R.string.share_confirm_public_link_button else R.string.action_share)
inviteMemberButton.contentDescription = getString(R.string.content_description_create_share_button)
}
it.selectedMember?.let { member ->
Expand Down Expand Up @@ -209,13 +234,19 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap
binding.inviteMemberButton.setOnClickListener {
uiState.selectedMember?.let { selectedMember ->
uiState.selectedRole?.let { selectedRole ->
graphShareViewModel.addGraphShare(selectedMember, selectedRole.id)
if (editMode) {
graphShareViewModel.editGraphShare(selectedShareId, selectedRole.id, uiState.selectedExpirationDate)
} else {
graphShareViewModel.addGraphShare(selectedMember, selectedRole.id)
}
}
}
}
}
}
}

private fun observeAddShareResult() {
collectLatestLifecycleFlow(graphShareViewModel.addShareResultFlow) { event ->
event?.peekContent()?.let { uiResult ->
when (uiResult) {
Expand All @@ -227,15 +258,52 @@ class AddGraphShareFragment : Fragment(), SearchMembersAdapter.SearchMembersAdap
}
}

private fun observeEditShareResult() {
collectLatestLifecycleFlow(graphShareViewModel.editShareResultFlow) { event ->
event?.peekContent()?.let { uiResult ->
when (uiResult) {
is UIResult.Loading -> { }
is UIResult.Success -> parentFragmentManager.popBackStack()
is UIResult.Error -> showErrorInSnackbar(R.string.share_edit_failed, uiResult.error)
}
}
}
}

private fun bindEditMode(share: MemberPermission, roles: List<OCRole>) {
selectedShareId = share.id
graphShareViewModel.onMemberSelected(share.toOCMember())

val selectedRole = roles.first { it.id == share.roles[0] }
graphShareViewModel.onRoleSelected(selectedRole)

share.expirationDateTime?.let { expirationDate ->
graphShareViewModel.onExpirationDateSelected(expirationDate)
binding.expirationDateLayout.expirationDateSwitch.isChecked = true
}
}

companion object {
private const val ARG_FILE = "FILE"
private const val ARG_ACCOUNT_NAME = "ACCOUNT_NAME"
private const val ARG_EDIT_MODE = "EDIT_MODE"
private const val ARG_SELECTED_SHARE = "SELECTED_SHARE"
private const val ARG_ROLES = "ROLES"
private const val DEFAULT_SEARCH_MIN_LENGTH = 3

fun newInstance(file: OCFile, accountName: String): AddGraphShareFragment {
fun newInstance(
file: OCFile,
accountName: String,
roles: List<OCRole>,
editMode: Boolean,
selectedShare: MemberPermission?
): AddGraphShareFragment {
val args = Bundle().apply {
putParcelable(ARG_FILE, file)
putString(ARG_ACCOUNT_NAME, accountName)
putParcelableArrayList(ARG_ROLES, ArrayList(roles))
putBoolean(ARG_EDIT_MODE, editMode)
putParcelable(ARG_SELECTED_SHARE, selectedShare)
}
return AddGraphShareFragment().apply {
arguments = args
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ 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.collectLatestLifecycleFlow
import com.owncloud.android.extensions.showErrorInSnackbar
import com.owncloud.android.extensions.showMessageInSnackbar
Expand All @@ -40,7 +41,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 +56,7 @@ class GraphShareFragment : Fragment() {

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

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = MembersFragmentBinding.inflate(inflater, container, false)
Expand All @@ -65,7 +67,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 All @@ -82,7 +84,7 @@ class GraphShareFragment : Fragment() {
binding.addMemberButton.setOnClickListener {
if (file != null && accountName != null) {
graphShareViewModel.resetViewModel()
listener?.addGraphShare(file = file, accountName = accountName)
listener?.addGraphShare(file = file, accountName = accountName, roles = roles, editMode = false, selectedShare = null)
}
}

Expand All @@ -104,56 +106,69 @@ class GraphShareFragment : Fragment() {
_binding = null
}

override fun onEditShare(share: MemberPermission) {
val file = requireArguments().getParcelable<OCFile>(ARG_FILE)
val accountName = requireArguments().getString(ARG_ACCOUNT_NAME)
if (file != null && accountName != null) {
graphShareViewModel.resetViewModel()
listener?.addGraphShare(file = file, accountName = accountName, roles = roles, editMode = true, selectedShare = share)
}
}

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

private fun observeRoles() {
collectLatestLifecycleFlow(graphShareViewModel.roles) { event ->
private fun observeShares() {
collectLatestLifecycleFlow(graphShareViewModel.shares) { event ->
event?.let {
when (val uiResult = event.peekContent()) {
is UIResult.Success -> {
uiResult.data?.let {
roles = it
graphShareViewModel.getGraphShares()
roles = it.roles
val hasMembers = it.members.isNotEmpty()
binding.membersRecyclerView.isVisible = hasMembers
binding.noSharesMessage.isVisible = !hasMembers
graphSharesAdapter.setShares(it.members, it.roles, canEditShares)
binding.swipeRefreshMembers.isRefreshing = false
}
}
is UIResult.Loading -> { }
is UIResult.Loading -> { binding.swipeRefreshMembers.isRefreshing = true }
is UIResult.Error -> {
binding.swipeRefreshMembers.isRefreshing = false
showErrorInSnackbar(R.string.share_sync_failed, uiResult.error)
Timber.e(uiResult.error, "Failed to retrieve platform roles")
Timber.e(uiResult.error, "Failed to retrieve shares")
}
}
}
}
}

private fun observeShares() {
collectLatestLifecycleFlow(graphShareViewModel.shares) { event ->
private fun observeSpacePermissions() {
collectLatestLifecycleFlow(graphShareViewModel.spacePermissions) { event ->
event?.let {
when (val uiResult = event.peekContent()) {
is UIResult.Success -> {
uiResult.data?.let {
val hasMembers = it.members.isNotEmpty()
binding.membersRecyclerView.isVisible = hasMembers
binding.noSharesMessage.isVisible = !hasMembers
graphSharesAdapter.setShares(it.members, it.roles)
binding.swipeRefreshMembers.isRefreshing = false
uiResult.data?.let { spacePermissions ->
checkPermissions(spacePermissions)
}
}
is UIResult.Loading -> { binding.swipeRefreshMembers.isRefreshing = true }
is UIResult.Loading -> { }
is UIResult.Error -> {
binding.swipeRefreshMembers.isRefreshing = false
showErrorInSnackbar(R.string.share_sync_failed, uiResult.error)
Timber.e(uiResult.error, "Failed to retrieve shares")
Timber.e(uiResult.error, "Failed to retrieve space permissions")
}
}
}
}
}

private fun checkPermissions(spacePermissions: List<String>) {
canEditShares = DRIVES_UPDATE_PERMISSION in spacePermissions
}

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

private fun observeEditShareResult() {
collectLatestLifecycleFlow(graphShareViewModel.editShareResultFlow) { event ->
event?.peekContent()?.let { uiResult ->
when (uiResult) {
is UIResult.Loading -> { }
is UIResult.Success -> {
showMessageInSnackbar(getString(R.string.share_edit_correctly))
graphShareViewModel.resetViewModel()
}
is UIResult.Error -> { }
}
}
}
}

interface GraphShareFragmentListener {
fun addGraphShare(file: OCFile, accountName: String)
fun addGraphShare(file: OCFile, accountName: String, roles: List<OCRole>, editMode: Boolean, selectedShare: MemberPermission?)
}

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

fun newInstance(file: OCFile, accountName: String): GraphShareFragment {
val args = Bundle().apply {
Expand Down
Loading
Loading