Skip to content

Commit 7771bc6

Browse files
authored
Merge pull request #4783 from owncloud/feature/list_links_over_a_space
[FEATURE REQUEST] List links over a space
2 parents 5a75bad + 1535225 commit 7771bc6

17 files changed

Lines changed: 538 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ ownCloud admins and users.
4141
## Summary
4242

4343
* Bugfix - Edit space option available when there is no connection: [#4742](https://github.com/owncloud/android/issues/4742)
44+
* Bugfix - Sort is not alphabetically correct: [#4760](https://github.com/owncloud/android/issues/4760)
4445
* Change - Migrate tests to the new kotlinx-coroutines-test API: [#4710](https://github.com/owncloud/android/issues/4710)
4546
* Change - Increase rating dialog delay: [#4744](https://github.com/owncloud/android/pull/4744)
4647
* Enhancement - Show members of a space: [#4612](https://github.com/owncloud/android/issues/4612)
@@ -49,6 +50,7 @@ ownCloud admins and users.
4950
* Enhancement - Edit a space member: [#4724](https://github.com/owncloud/android/issues/4724)
5051
* Enhancement - Remove a space member: [#4725](https://github.com/owncloud/android/issues/4725)
5152
* Enhancement - Workflow to build APK: [#4751](https://github.com/owncloud/android/pull/4751)
53+
* Enhancement - List links over a space: [#4752](https://github.com/owncloud/android/issues/4752)
5254
* Enhancement - Copy permanent link of a space: [#4758](https://github.com/owncloud/android/issues/4758)
5355
* Enhancement - Workflow to check Conventional Commits: [#4759](https://github.com/owncloud/android/pull/4759)
5456
* Enhancement - QA Content Provider: [#4776](https://github.com/owncloud/android/pull/4776)
@@ -63,6 +65,14 @@ ownCloud admins and users.
6365
https://github.com/owncloud/android/issues/4742
6466
https://github.com/owncloud/android/pull/4750
6567

68+
* Bugfix - Sort is not alphabetically correct: [#4760](https://github.com/owncloud/android/issues/4760)
69+
70+
Locale-aware collation has been applied to file sorting by name to handle
71+
accented characters correctly.
72+
73+
https://github.com/owncloud/android/issues/4760
74+
https://github.com/owncloud/android/pull/4785
75+
6676
* Change - Migrate tests to the new kotlinx-coroutines-test API: [#4710](https://github.com/owncloud/android/issues/4710)
6777

6878
Some tests from view model classes have been refactored in order to use the new
@@ -137,6 +147,15 @@ ownCloud admins and users.
137147

138148
https://github.com/owncloud/android/pull/4751
139149

150+
* Enhancement - List links over a space: [#4752](https://github.com/owncloud/android/issues/4752)
151+
152+
The list of all available space public links has been added below the list of
153+
space members. This new section shows information like public link names, types
154+
and expiration date (if available).
155+
156+
https://github.com/owncloud/android/issues/4752
157+
https://github.com/owncloud/android/pull/4783
158+
140159
* Enhancement - Copy permanent link of a space: [#4758](https://github.com/owncloud/android/issues/4758)
141160

142161
A new option to copy and share the permanent link of a space has been added next

changelog/unreleased/4783

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Enhancement: List links over a space
2+
3+
The list of all available space public links has been added below the list of space members.
4+
This new section shows information like public link names, types and expiration date (if available).
5+
6+
https://github.com/owncloud/android/issues/4752
7+
https://github.com/owncloud/android/pull/4783
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.extensions
22+
23+
import com.owncloud.android.R
24+
import com.owncloud.android.domain.links.model.OCLinkType
25+
26+
fun OCLinkType.toStringResId() =
27+
when (this) {
28+
OCLinkType.CAN_VIEW -> R.string.public_link_view
29+
OCLinkType.CAN_EDIT -> R.string.public_link_edit
30+
OCLinkType.CREATE_ONLY -> R.string.public_link_create_only
31+
OCLinkType.CAN_UPLOAD -> R.string.public_link_upload
32+
OCLinkType.INTERNAL -> R.string.public_link_internal
33+
}

owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class ReleaseNotesViewModel(
4848
subtitle = R.string.release_notes_4_8_0_subtitle_space_membership,
4949
type = ReleaseNoteType.ENHANCEMENT
5050
),
51+
ReleaseNote(
52+
title = R.string.release_notes_4_8_0_title_space_public_links,
53+
subtitle = R.string.release_notes_4_8_0_subtitle_space_public_links,
54+
type = ReleaseNoteType.ENHANCEMENT
55+
),
5156
ReleaseNote(
5257
title = R.string.release_notes_4_8_0_title_set_emoji_as_space_image,
5358
subtitle = R.string.release_notes_4_8_0_subtitle_set_emoji_as_space_image,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.spaces.links
22+
23+
import android.view.LayoutInflater
24+
import android.view.View
25+
import android.view.ViewGroup
26+
import androidx.core.view.isVisible
27+
import androidx.recyclerview.widget.DiffUtil
28+
import androidx.recyclerview.widget.RecyclerView
29+
import com.owncloud.android.R
30+
import com.owncloud.android.databinding.PublicLinkItemBinding
31+
import com.owncloud.android.domain.links.model.OCLink
32+
import com.owncloud.android.extensions.toStringResId
33+
import com.owncloud.android.utils.DisplayUtils
34+
import com.owncloud.android.utils.PreferenceUtils
35+
36+
class SpaceLinksAdapter(
37+
private val listener: SpaceLinksAdapterListener
38+
): RecyclerView.Adapter<SpaceLinksAdapter.SpaceLinksViewHolder>() {
39+
40+
private var spaceLinks: List<OCLink> = emptyList()
41+
42+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SpaceLinksViewHolder {
43+
val inflater = LayoutInflater.from(parent.context)
44+
45+
val view = inflater.inflate(R.layout.public_link_item, parent, false)
46+
view.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(parent.context)
47+
48+
return SpaceLinksViewHolder(view)
49+
}
50+
51+
override fun onBindViewHolder(holder: SpaceLinksViewHolder, position: Int) {
52+
val spaceLink = spaceLinks[position]
53+
holder.binding.apply {
54+
publicLinkDisplayName.text = spaceLink.displayName
55+
publicLinkType.text = holder.itemView.context.getString(spaceLink.type.toStringResId())
56+
57+
val hasExpirationDate = spaceLink.expirationDateTime != null
58+
expirationCalendarIcon.isVisible = hasExpirationDate
59+
expirationDate.isVisible = hasExpirationDate
60+
if (hasExpirationDate) {
61+
expirationDate.apply {
62+
text = DisplayUtils.displayDateToHumanReadable(spaceLink.expirationDateTime)
63+
contentDescription = holder.itemView.context.getString(R.string.content_description_member_expiration_date, expirationDate.text)
64+
}
65+
}
66+
copyPublicLinkButton.apply {
67+
contentDescription = holder.itemView.context.getString(R.string.content_description_get_public_link, spaceLink.displayName)
68+
setOnClickListener {
69+
listener.onCopyOrSendPublicLink(spaceLink.webUrl)
70+
}
71+
}
72+
}
73+
}
74+
75+
override fun getItemCount(): Int = spaceLinks.size
76+
77+
fun setSpaceLinks(spaceLinks: List<OCLink>) {
78+
val diffCallback = SpaceLinksDiffUtil(this.spaceLinks, spaceLinks)
79+
val diffResult = DiffUtil.calculateDiff(diffCallback)
80+
this.spaceLinks = spaceLinks
81+
diffResult.dispatchUpdatesTo(this)
82+
}
83+
84+
class SpaceLinksViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
85+
val binding = PublicLinkItemBinding.bind(itemView)
86+
}
87+
88+
interface SpaceLinksAdapterListener {
89+
fun onCopyOrSendPublicLink(publicLinkUrl: String)
90+
}
91+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.spaces.links
22+
23+
import androidx.recyclerview.widget.DiffUtil
24+
import com.owncloud.android.domain.links.model.OCLink
25+
26+
class SpaceLinksDiffUtil(
27+
private val oldList: List<OCLink>,
28+
private val newList: List<OCLink>
29+
) : DiffUtil.Callback() {
30+
override fun getOldListSize(): Int = oldList.size
31+
32+
override fun getNewListSize(): Int = newList.size
33+
34+
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
35+
val oldItem = oldList[oldItemPosition]
36+
val newItem = newList[newItemPosition]
37+
38+
return oldItem.id == newItem.id
39+
}
40+
41+
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
42+
val oldItem = oldList[oldItemPosition]
43+
val newItem = newList[newItemPosition]
44+
45+
return ((oldItem.id == newItem.id) && (oldItem.expirationDateTime == newItem.expirationDateTime) &&
46+
(oldItem.displayName == newItem.displayName) && (oldItem.type == newItem.type) && (oldItem.webUrl == newItem.webUrl))
47+
}
48+
}

owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class SpaceMembersActivity: FileActivity(), SpaceMembersFragment.SpaceMemberFrag
6767
}
6868

6969
permanentLinkButton.setOnClickListener {
70-
copyOrSendPermanentLink(currentSpace.webUrl, currentSpace.name)
70+
copyOrSendLink(currentSpace.webUrl, currentSpace.name)
7171
}
7272
}
7373

@@ -100,18 +100,22 @@ class SpaceMembersActivity: FileActivity(), SpaceMembersFragment.SpaceMemberFrag
100100
}
101101
}
102102

103-
private fun copyOrSendPermanentLink(permanentLink: String?, spaceName: String) {
104-
permanentLink?.let {
103+
override fun copyOrSendPublicLink(publicLinkUrl: String, spaceName: String) {
104+
copyOrSendLink(publicLinkUrl, spaceName)
105+
}
106+
107+
private fun copyOrSendLink(link: String?, spaceName: String) {
108+
link?.let {
105109
val displayName = AccountManager.get(this).getUserData(account, KEY_DISPLAY_NAME)
106110

107-
val intentToSharePermanentLink = Intent(Intent.ACTION_SEND).apply {
111+
val intentToShareLink = Intent(Intent.ACTION_SEND).apply {
108112
type = TYPE_PLAIN
109113
putExtra(Intent.EXTRA_TEXT, it)
110114
putExtra(Intent.EXTRA_SUBJECT, getString(R.string.subject_user_shared_with_you, displayName, spaceName))
111115
}
112116

113117
val shareSheetIntent = ShareSheetHelper().getShareSheetIntent(
114-
intent = intentToSharePermanentLink,
118+
intent = intentToShareLink,
115119
context = this,
116120
title = R.string.activity_chooser_title,
117121
packagesToExclude = arrayOf(packageName)

owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ import android.view.ViewGroup
2929
import androidx.core.view.isVisible
3030
import androidx.fragment.app.Fragment
3131
import androidx.recyclerview.widget.LinearLayoutManager
32-
import androidx.recyclerview.widget.RecyclerView
3332
import com.owncloud.android.R
3433
import com.owncloud.android.databinding.MembersFragmentBinding
34+
import com.owncloud.android.domain.links.model.OCLink
3535
import com.owncloud.android.domain.roles.model.OCRole
3636
import com.owncloud.android.domain.roles.model.OCRoleType
3737
import com.owncloud.android.domain.spaces.model.OCSpace
@@ -41,11 +41,17 @@ import com.owncloud.android.extensions.collectLatestLifecycleFlow
4141
import com.owncloud.android.extensions.showErrorInSnackbar
4242
import com.owncloud.android.extensions.showMessageInSnackbar
4343
import com.owncloud.android.presentation.common.UIResult
44+
import com.owncloud.android.presentation.spaces.links.SpaceLinksAdapter
45+
import com.owncloud.android.utils.DisplayUtils
4446
import org.koin.androidx.viewmodel.ext.android.activityViewModel
4547
import org.koin.core.parameter.parametersOf
4648
import timber.log.Timber
49+
import java.text.SimpleDateFormat
50+
import java.util.Date
51+
import java.util.Locale
52+
import java.util.TimeZone
4753

48-
class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapterListener {
54+
class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapterListener, SpaceLinksAdapter.SpaceLinksAdapterListener {
4955
private var _binding: MembersFragmentBinding? = null
5056
private val binding get() = _binding!!
5157

@@ -57,7 +63,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
5763
}
5864

5965
private lateinit var spaceMembersAdapter: SpaceMembersAdapter
60-
private lateinit var recyclerView: RecyclerView
66+
private lateinit var spaceLinksAdapter: SpaceLinksAdapter
6167
private lateinit var currentSpace: OCSpace
6268

6369
private var roles: List<OCRole> = emptyList()
@@ -77,12 +83,17 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
7783
super.onViewCreated(view, savedInstanceState)
7884
val accountId = requireArguments().getString(ARG_ACCOUNT_ID)
7985
spaceMembersAdapter = SpaceMembersAdapter(this, accountId)
80-
recyclerView = binding.membersRecyclerView
81-
recyclerView.apply {
86+
binding.membersRecyclerView.apply {
8287
layoutManager = LinearLayoutManager(requireContext())
8388
adapter = spaceMembersAdapter
8489
}
8590

91+
spaceLinksAdapter = SpaceLinksAdapter(this)
92+
binding.publicLinksRecyclerView.apply {
93+
layoutManager = LinearLayoutManager(requireContext())
94+
adapter = spaceLinksAdapter
95+
}
96+
8697
currentSpace = requireArguments().getParcelable<OCSpace>(ARG_CURRENT_SPACE) ?: return
8798
savedInstanceState?.let {
8899
canRemoveMembers = it.getBoolean(CAN_REMOVE_MEMBERS, false)
@@ -150,6 +161,10 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
150161
)
151162
}
152163

164+
override fun onCopyOrSendPublicLink(publicLinkUrl: String) {
165+
listener?.copyOrSendPublicLink(publicLinkUrl, currentSpace.name)
166+
}
167+
153168
private fun subscribeToViewModels() {
154169
collectLatestLifecycleFlow(spaceMembersViewModel.roles) { event ->
155170
event?.let {
@@ -179,10 +194,14 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
179194
spaceMembers = it.members
180195
addMemberRoles = it.roles
181196
spaceMembersAdapter.setSpaceMembers(spaceMembers, roles, canRemoveMembers, canEditMembers, numberOfManagers)
197+
val hasLinks = it.links.isNotEmpty()
198+
showOrHideEmptyView(hasLinks)
199+
if (hasLinks) { showSpaceLinks(it.links) }
200+
binding.indeterminateProgressBar.isVisible = false
182201
}
183202
}
184203
}
185-
is UIResult.Loading -> { }
204+
is UIResult.Loading -> { binding.indeterminateProgressBar.isVisible = true }
186205
is UIResult.Error -> {
187206
requireActivity().finish()
188207
Timber.e(uiResult.error, "Failed to retrieve space members for space: ${currentSpace.id} (${currentSpace.id})")
@@ -251,8 +270,29 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
251270
}
252271
}
253272

273+
private fun showOrHideEmptyView(hasLinks: Boolean) {
274+
binding.apply {
275+
publicLinksRecyclerView.isVisible = hasLinks
276+
noPublicLinksMessage.isVisible = !hasLinks
277+
}
278+
}
279+
280+
private fun showSpaceLinks(spaceLinks: List<OCLink>) {
281+
val formatter = SimpleDateFormat(DisplayUtils.DATE_FORMAT_ISO, Locale.ROOT).apply {
282+
timeZone = TimeZone.getTimeZone("UTC")
283+
}
284+
spaceLinksAdapter.setSpaceLinks(spaceLinks.sortedByDescending { spaceLink ->
285+
if (spaceLink.createdDateTime.isNotEmpty()) {
286+
formatter.parse(spaceLink.createdDateTime)
287+
} else {
288+
Date(0)
289+
}
290+
})
291+
}
292+
254293
interface SpaceMemberFragmentListener {
255294
fun addMember(space: OCSpace, spaceMembers: List<SpaceMember>, roles: List<OCRole>, editMode: Boolean, selectedMember: SpaceMember?)
295+
fun copyOrSendPublicLink(publicLinkUrl: String, spaceName: String)
256296
}
257297

258298
companion object {

0 commit comments

Comments
 (0)