Skip to content
Open
6 changes: 6 additions & 0 deletions changelog/unreleased/4817
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Edit space link

A new option to edit space public links has been added. It will be only visible for users with proper permissions.

https://github.com/owncloud/android/issues/4756
https://github.com/owncloud/android/pull/4817
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ import com.owncloud.android.domain.files.usecases.SortFilesUseCase
import com.owncloud.android.domain.files.usecases.SortFilesWithSyncInfoUseCase
import com.owncloud.android.domain.files.usecases.UpdateAlreadyDownloadedFilesPathUseCase
import com.owncloud.android.domain.links.usecases.AddLinkUseCase
import com.owncloud.android.domain.links.usecases.EditLinkUseCase
import com.owncloud.android.domain.links.usecases.EditPasswordLinkUseCase
import com.owncloud.android.domain.links.usecases.RemoveLinkUseCase
import com.owncloud.android.domain.members.usecases.AddMemberUseCase
import com.owncloud.android.domain.members.usecases.EditMemberUseCase
Expand Down Expand Up @@ -321,5 +323,7 @@ val useCaseModule = module {

// Links
factoryOf(::AddLinkUseCase)
factoryOf(::EditLinkUseCase)
factoryOf(::EditPasswordLinkUseCase)
factoryOf(::RemoveLinkUseCase)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ package com.owncloud.android.presentation.capabilities
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.ViewModel
import com.owncloud.android.domain.capabilities.model.CapabilityBooleanType
import com.owncloud.android.domain.capabilities.model.OCCapability
import com.owncloud.android.domain.capabilities.usecases.GetCapabilitiesAsLiveDataUseCase
import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
import com.owncloud.android.domain.capabilities.usecases.RefreshCapabilitiesFromServerAsyncUseCase
import com.owncloud.android.domain.links.model.OCLinkType
import com.owncloud.android.domain.utils.Event
import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResultAndUseCachedData
import com.owncloud.android.presentation.common.UIResult
Expand Down Expand Up @@ -83,11 +81,4 @@ class CapabilityViewModel(
capabilities?.spaces?.hasMultiplePersonalSpaces == true
}

fun checkPasswordEnforced(selectedPermission: OCLinkType, capabilities: OCCapability?) =
when(selectedPermission) {
OCLinkType.CAN_VIEW -> capabilities?.filesSharingPublicPasswordEnforcedReadOnly == CapabilityBooleanType.TRUE
OCLinkType.CAN_EDIT -> capabilities?.filesSharingPublicPasswordEnforcedReadWrite == CapabilityBooleanType.TRUE
OCLinkType.CREATE_ONLY -> capabilities?.filesSharingPublicPasswordEnforcedUploadOnly == CapabilityBooleanType.TRUE
else -> true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,16 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.owncloud.android.R
import com.owncloud.android.databinding.AddPublicLinkFragmentBinding
import com.owncloud.android.domain.capabilities.model.OCCapability
import com.owncloud.android.domain.links.model.OCLink
import com.owncloud.android.domain.links.model.OCLinkType
import com.owncloud.android.domain.spaces.model.OCSpace
import com.owncloud.android.extensions.collectLatestLifecycleFlow
import com.owncloud.android.extensions.hideSoftKeyboard
import com.owncloud.android.extensions.showErrorInSnackbar
import com.owncloud.android.presentation.capabilities.CapabilityViewModel
import com.owncloud.android.presentation.common.UIResult
import com.owncloud.android.utils.DisplayUtils
import org.koin.androidx.viewmodel.ext.android.activityViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
Expand All @@ -60,15 +57,11 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi
requireArguments().getParcelable(ARG_CURRENT_SPACE)
)
}
private val capabilityViewModel: CapabilityViewModel by viewModel {
parametersOf(
accountName
)
}

private var capabilities: OCCapability? = null
private var isPasswordEnforced = true
private var hasPassword = false
private var editMode = false
private var selectedPublicLink: OCLink? = null

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = AddPublicLinkFragmentBinding.inflate(inflater, container, false)
Expand All @@ -77,7 +70,9 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().setTitle(R.string.public_link_create_title)
editMode = requireArguments().getBoolean(ARG_EDIT_MODE)
selectedPublicLink = requireArguments().getParcelable(ARG_SELECTED_PUBLIC_LINK)
requireActivity().setTitle(if (editMode) R.string.public_link_edit_title else R.string.public_link_create_title)

binding.publicLinkPermissions.apply {
canViewPublicLinkRadioButton.tag = OCLinkType.CAN_VIEW
Expand All @@ -94,7 +89,7 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi
}
}

hasPassword = it.selectedPassword != null
hasPassword = it.hasPassword
it.selectedPermission?.let {
binding.optionsLayout.isVisible = true
binding.passwordLayout.apply {
Expand All @@ -115,6 +110,7 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi
openDatePickerDialog(uiState.selectedExpirationDate)
} else {
expirationDateSwitch.isChecked = true
openDatePickerDialog(null)
}
}
}
Expand All @@ -130,28 +126,28 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi
}
}

capabilityViewModel.capabilities.observe(viewLifecycleOwner) { event->
when (val uiResult = event.peekContent()) {
is UIResult.Success -> {
capabilities = uiResult.data
}
is UIResult.Loading -> { }
is UIResult.Error -> {
Timber.e(uiResult.error, "Failed to retrieve server capabilities")
collectLatestLifecycleFlow(spaceLinksViewModel.addLinkResultFlow) { event ->
event?.peekContent()?.let { uiResult ->
when (uiResult) {
is UIResult.Loading -> { }
is UIResult.Success -> parentFragmentManager.popBackStack()
is UIResult.Error -> showErrorInSnackbar(R.string.public_link_add_failed, uiResult.error)
}
}
}

collectLatestLifecycleFlow(spaceLinksViewModel.addLinkResultFlow) { event ->
collectLatestLifecycleFlow(spaceLinksViewModel.editLinkResultFlow) { event ->
event?.peekContent()?.let { uiResult ->
when (uiResult) {
is UIResult.Loading -> { }
is UIResult.Success -> parentFragmentManager.popBackStack()
is UIResult.Error -> showErrorInSnackbar(R.string.public_link_add_failed, uiResult.error)
is UIResult.Error -> showErrorInSnackbar(R.string.public_link_edit_failed, uiResult.error)
}
}
}

if (editMode) { bindEditMode() }

binding.publicLinkPermissions.apply {
canViewPublicLinkRadioButton.setOnClickListener { selectRadioButton(canViewPublicLinkRadioButton) }
canViewPublicLinkLayout.setOnClickListener { selectRadioButton(canViewPublicLinkRadioButton) }
Expand All @@ -174,9 +170,12 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi
}

binding.createPublicLinkButton.setOnClickListener {
spaceLinksViewModel.createPublicLink(
binding.publicLinkNameEditText.text.toString().ifEmpty { getString(R.string.public_link_default_display_name) }
)
val displayName = binding.publicLinkNameEditText.text.toString().ifEmpty { getString(R.string.public_link_default_display_name) }
if (editMode) {
selectedPublicLink?.let { spaceLinksViewModel.editPublicLink(it.id, displayName) }
} else {
spaceLinksViewModel.createPublicLink(displayName)
}
}
}

Expand All @@ -188,10 +187,11 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi

override fun onSetPassword(password: String) {
val normalizedPassword = password.ifBlank { null }
if (!isPasswordEnforced && normalizedPassword == null) {
val hasPassword = normalizedPassword != null
if (!isPasswordEnforced && !hasPassword) {
binding.passwordLayout.setPasswordSwitch.isChecked = false
}
spaceLinksViewModel.onPasswordSelected(normalizedPassword)
spaceLinksViewModel.onPasswordSelected(normalizedPassword, hasPassword)
}

private fun selectRadioButton(selectedRadioButton: RadioButton) {
Expand All @@ -203,14 +203,14 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi
selectedRadioButton.isChecked = true
}
val selectedPermission = selectedRadioButton.tag as OCLinkType
isPasswordEnforced = capabilityViewModel.checkPasswordEnforced(selectedPermission, capabilities)
isPasswordEnforced = spaceLinksViewModel.checkPasswordEnforced(selectedPermission)
spaceLinksViewModel.onPermissionSelected(selectedPermission)
}

private fun bindDatePickerDialog(expirationDate: String?) {
binding.expirationDateLayout.expirationDateSwitch.setOnCheckedChangeListener { _, isChecked ->
binding.expirationDateLayout.expirationDateSwitch.setOnClickListener {
hideKeyboardAndClearFocus()
if (isChecked) {
if (binding.expirationDateLayout.expirationDateSwitch.isChecked) {
openDatePickerDialog(expirationDate)
} else {
binding.expirationDateLayout.expirationDateValue.visibility = View.GONE
Expand Down Expand Up @@ -263,26 +263,61 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi

private fun removePassword() {
hideKeyboardAndClearFocus()
spaceLinksViewModel.onPasswordSelected(null)
spaceLinksViewModel.onPasswordSelected(null, false)
}

private fun hideKeyboardAndClearFocus() {
hideSoftKeyboard()
binding.publicLinkNameEditText.clearFocus()
}

private fun bindEditMode() {
selectedPublicLink?.let {
binding.publicLinkNameEditText.setText(it.displayName)
binding.createPublicLinkButton.apply {
setText(R.string.share_confirm_public_link_button)
contentDescription = getString(R.string.share_confirm_public_link_button)
}

// Do not recreate the edit view after the first iteration
if (spaceLinksViewModel.addPublicLinkUIState.value?.selectedPermission != null) return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this condition to be the first in the bock to be checked? if no permission to return even before the name and the button are binded

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is used to avoid re-creating the edit view when the device is rotated, but the button still needs to be bound because the default label is CREATE (it should be SAVE in edit mode). The text binding could be moved below the condition.

New version:

binding.createPublicLinkButton.apply { ... }

// Do not recreate the edit view after the first iteration
if (spaceLinksViewModel.addPublicLinkUIState.value?.selectedPermission != null) return

binding.publicLinkNameEditText.setText(it.displayName)


when (it.type) {
OCLinkType.CAN_VIEW -> selectRadioButton(binding.publicLinkPermissions.canViewPublicLinkRadioButton)
OCLinkType.CAN_EDIT -> selectRadioButton(binding.publicLinkPermissions.canEditPublicLinkRadioButton)
OCLinkType.CREATE_ONLY -> selectRadioButton(binding.publicLinkPermissions.secretFileDropPublicLinkRadioButton)
else -> {}
}

if (it.hasPassword) {
spaceLinksViewModel.onPasswordSelected(password = null, hasPassword = true, wasPasswordChanged = false)
}

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

companion object {
private const val DIALOG_SET_PASSWORD = "DIALOG_SET_PASSWORD"
private const val ARG_ACCOUNT_NAME = "ARG_ACCOUNT_NAME"
private const val ARG_CURRENT_SPACE = "ARG_CURRENT_SPACE"
private const val ARG_EDIT_MODE = "ARG_EDIT_MODE"
private const val ARG_SELECTED_PUBLIC_LINK = "ARG_SELECTED_PUBLIC_LINK"

fun newInstance(
accountName: String,
currentSpace: OCSpace
currentSpace: OCSpace,
editMode: Boolean,
selectedPublicLink: OCLink?
): AddPublicLinkFragment {
val args = Bundle().apply {
putString(ARG_ACCOUNT_NAME, accountName)
putParcelable(ARG_CURRENT_SPACE, currentSpace)
putBoolean(ARG_EDIT_MODE, editMode)
putParcelable(ARG_SELECTED_PUBLIC_LINK, selectedPublicLink)
}
return AddPublicLinkFragment().apply {
arguments = args
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss OnDestroyView in this fragment (not part of this specific review but...)

Copy link
Copy Markdown
Contributor Author

@joragua joragua Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! Added ✅ I've added onDestroyView method in SetPasswordDialogFragment too

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class SpaceLinksAdapter(

private var spaceLinks: List<OCLink> = emptyList()
private var canRemoveLinks = false
private var canEditLinks = false

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SpaceLinksViewHolder {
val inflater = LayoutInflater.from(parent.context)
Expand Down Expand Up @@ -78,13 +79,22 @@ class SpaceLinksAdapter(
listener.onRemovePublicLink(spaceLink.id, spaceLink.displayName)
}
}

editPublicLinkButton.apply {
contentDescription = holder.itemView.context.getString(R.string.content_description_edit_public_link, spaceLink.displayName)
isVisible = canEditLinks
setOnClickListener {
listener.onEditPublicLink(spaceLink)
}
}
}
}

override fun getItemCount(): Int = spaceLinks.size

fun setSpaceLinks(spaceLinks: List<OCLink>, canRemoveLinks: Boolean) {
fun setSpaceLinks(spaceLinks: List<OCLink>, canRemoveLinks: Boolean, canEditLinks: Boolean) {
this.canRemoveLinks = canRemoveLinks
this.canEditLinks = canEditLinks
val diffCallback = SpaceLinksDiffUtil(this.spaceLinks, spaceLinks)
val diffResult = DiffUtil.calculateDiff(diffCallback)
this.spaceLinks = spaceLinks
Expand All @@ -98,5 +108,6 @@ class SpaceLinksAdapter(
interface SpaceLinksAdapterListener {
fun onCopyOrSendPublicLink(publicLinkUrl: String)
fun onRemovePublicLink(publicLinkId: String, publicLinkDisplayName: String)
fun onEditPublicLink(publicLink: OCLink)
}
}
Loading
Loading