Skip to content

Commit 68e1d19

Browse files
committed
feat: display space public links in the list
1 parent 94dcb97 commit 68e1d19

10 files changed

Lines changed: 381 additions & 7 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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: RecyclerView.Adapter<SpaceLinksAdapter.SpaceLinksViewHolder>() {
37+
38+
private var spaceLinks: List<OCLink> = emptyList()
39+
40+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SpaceLinksViewHolder {
41+
val inflater = LayoutInflater.from(parent.context)
42+
43+
val view = inflater.inflate(R.layout.public_link_item, parent, false)
44+
view.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(parent.context)
45+
46+
return SpaceLinksViewHolder(view)
47+
}
48+
49+
override fun onBindViewHolder(holder: SpaceLinksViewHolder, position: Int) {
50+
val spaceLink = spaceLinks[position]
51+
holder.binding.apply {
52+
publicLinkDisplayName.text = spaceLink.displayName
53+
publicLinkType.text = holder.itemView.context.getString(spaceLink.type.toStringResId())
54+
55+
val hasExpirationDate = spaceLink.expirationDateTime != null
56+
expirationCalendarIcon.isVisible = hasExpirationDate
57+
expirationDate.isVisible = hasExpirationDate
58+
if (hasExpirationDate) {
59+
expirationDate.apply {
60+
text = DisplayUtils.displayDateToHumanReadable(spaceLink.expirationDateTime)
61+
contentDescription = holder.itemView.context.getString(R.string.content_description_member_expiration_date, expirationDate.text)
62+
}
63+
}
64+
}
65+
}
66+
67+
override fun getItemCount(): Int = spaceLinks.size
68+
69+
fun setSpaceLinks(spaceLinks: List<OCLink>) {
70+
val diffCallback = SpaceLinksDiffUtil(this.spaceLinks, spaceLinks)
71+
val diffResult = DiffUtil.calculateDiff(diffCallback)
72+
this.spaceLinks = spaceLinks
73+
diffResult.dispatchUpdatesTo(this)
74+
}
75+
76+
class SpaceLinksViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
77+
val binding = PublicLinkItemBinding.bind(itemView)
78+
}
79+
}
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/SpaceMembersFragment.kt

Lines changed: 33 additions & 4 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,9 +41,14 @@ 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.Locale
51+
import java.util.TimeZone
4752

4853
class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapterListener {
4954
private var _binding: MembersFragmentBinding? = null
@@ -57,7 +62,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
5762
}
5863

5964
private lateinit var spaceMembersAdapter: SpaceMembersAdapter
60-
private lateinit var recyclerView: RecyclerView
65+
private lateinit var spaceLinksAdapter: SpaceLinksAdapter
6166
private lateinit var currentSpace: OCSpace
6267

6368
private var roles: List<OCRole> = emptyList()
@@ -77,12 +82,17 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
7782
super.onViewCreated(view, savedInstanceState)
7883
val accountId = requireArguments().getString(ARG_ACCOUNT_ID)
7984
spaceMembersAdapter = SpaceMembersAdapter(this, accountId)
80-
recyclerView = binding.membersRecyclerView
81-
recyclerView.apply {
85+
binding.membersRecyclerView.apply {
8286
layoutManager = LinearLayoutManager(requireContext())
8387
adapter = spaceMembersAdapter
8488
}
8589

90+
spaceLinksAdapter = SpaceLinksAdapter()
91+
binding.publicLinksRecyclerView.apply {
92+
layoutManager = LinearLayoutManager(requireContext())
93+
adapter = spaceLinksAdapter
94+
}
95+
8696
currentSpace = requireArguments().getParcelable<OCSpace>(ARG_CURRENT_SPACE) ?: return
8797
savedInstanceState?.let {
8898
canRemoveMembers = it.getBoolean(CAN_REMOVE_MEMBERS, false)
@@ -179,6 +189,9 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
179189
spaceMembers = it.members
180190
addMemberRoles = it.roles
181191
spaceMembersAdapter.setSpaceMembers(spaceMembers, roles, canRemoveMembers, canEditMembers, numberOfManagers)
192+
val hasLinks = it.links.isNotEmpty()
193+
showOrHideEmptyView(hasLinks)
194+
if (hasLinks) { showSpaceLinks(it.links) }
182195
}
183196
}
184197
}
@@ -251,6 +264,22 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter
251264
}
252265
}
253266

267+
private fun showOrHideEmptyView(hasLinks: Boolean) {
268+
binding.apply {
269+
publicLinksRecyclerView.isVisible = hasLinks
270+
noPublicLinksMessage.isVisible = !hasLinks
271+
}
272+
}
273+
274+
private fun showSpaceLinks(spaceLinks: List<OCLink>) {
275+
val formatter = SimpleDateFormat(DisplayUtils.DATE_FORMAT_ISO, Locale.ROOT).apply {
276+
timeZone = TimeZone.getTimeZone("UTC")
277+
}
278+
spaceLinksAdapter.setSpaceLinks(spaceLinks.sortedByDescending { spaceLink ->
279+
formatter.parse(spaceLink.createdDateTime)
280+
})
281+
}
282+
254283
interface SpaceMemberFragmentListener {
255284
fun addMember(space: OCSpace, spaceMembers: List<SpaceMember>, roles: List<OCRole>, editMode: Boolean, selectedMember: SpaceMember?)
256285
}

owncloudApp/src/main/res/layout/members_fragment.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@
125125
android:paddingStart="@dimen/standard_half_padding"
126126
android:paddingEnd="@dimen/standard_half_padding"
127127
android:text="@string/share_no_public_links"
128-
android:textSize="15sp" />
128+
android:textSize="15sp"
129+
android:visibility="gone"/>
129130

130131
</LinearLayout>
131132

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
ownCloud Android client application
3+
4+
Copyright (C) 2026 ownCloud GmbH.
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License version 2,
8+
as published by the Free Software Foundation.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
-->
18+
19+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
20+
android:layout_width="match_parent"
21+
android:layout_height="wrap_content"
22+
android:orientation="vertical"
23+
android:filterTouchesWhenObscured="true"
24+
xmlns:tools="http://schemas.android.com/tools">
25+
26+
<LinearLayout
27+
android:id="@+id/public_link_item_layout"
28+
android:layout_width="match_parent"
29+
android:layout_height="wrap_content"
30+
android:layout_marginTop="@dimen/standard_quarter_margin"
31+
android:orientation="horizontal"
32+
android:focusable="true">
33+
34+
<LinearLayout
35+
android:layout_width="0dp"
36+
android:layout_height="wrap_content"
37+
android:layout_weight="1"
38+
android:orientation="vertical"
39+
android:layout_marginStart="@dimen/standard_half_margin"
40+
android:layout_marginEnd="@dimen/standard_half_margin">
41+
42+
<TextView
43+
android:id="@+id/public_link_display_name"
44+
android:layout_width="wrap_content"
45+
android:layout_height="wrap_content"
46+
android:layout_marginTop="@dimen/standard_half_margin"
47+
android:layout_marginStart="@dimen/standard_half_margin"
48+
android:layout_gravity="center_vertical"
49+
android:text="@string/placeholder_filename"
50+
android:textSize="@dimen/two_line_primary_text_size"
51+
android:textColor="@color/textColor"
52+
android:textStyle="bold"
53+
android:ellipsize="middle"
54+
android:maxLines="1"/>
55+
56+
<LinearLayout
57+
android:layout_width="wrap_content"
58+
android:layout_height="wrap_content"
59+
android:orientation="horizontal">
60+
61+
<TextView
62+
android:id="@+id/public_link_type"
63+
android:layout_width="wrap_content"
64+
android:layout_height="wrap_content"
65+
android:layout_marginStart="@dimen/standard_half_margin"
66+
android:layout_marginEnd="@dimen/standard_half_margin"
67+
android:layout_marginBottom="@dimen/standard_half_margin"
68+
android:text="@string/placeholder_sentence"
69+
android:textSize="13sp"
70+
android:textColor="@color/textColor"
71+
android:ellipsize="middle"
72+
android:maxLines="1"/>
73+
74+
<ImageView
75+
android:id="@+id/expiration_calendar_icon"
76+
android:layout_width="20dp"
77+
android:layout_height="20dp"
78+
android:layout_marginEnd="@dimen/standard_half_margin"
79+
android:src="@drawable/file_calendar"
80+
android:visibility="gone"
81+
tools:visibility="visible"/>
82+
83+
<TextView
84+
android:id="@+id/expiration_date"
85+
android:layout_width="wrap_content"
86+
android:layout_height="wrap_content"
87+
android:text="@string/placeholder_sentence"
88+
android:textSize="13sp"
89+
android:textColor="@color/textColor"
90+
android:ellipsize="middle"
91+
android:visibility="gone"
92+
tools:visibility="visible"/>
93+
94+
</LinearLayout>
95+
96+
</LinearLayout>
97+
98+
<ImageButton
99+
android:id="@+id/copy_public_link_button"
100+
android:layout_width="40dp"
101+
android:layout_height="40dp"
102+
android:layout_gravity="center_vertical"
103+
android:layout_marginStart="@dimen/standard_half_margin"
104+
android:layout_marginEnd="@dimen/standard_margin"
105+
android:scaleType="centerCrop"
106+
android:padding="@dimen/standard_half_padding"
107+
android:background="?android:attr/selectableItemBackground"
108+
android:focusable="true"
109+
android:src="@drawable/copy_link"
110+
android:visibility="visible"/>
111+
112+
</LinearLayout>
113+
114+
<View
115+
android:layout_width="match_parent"
116+
android:layout_height="1dp"
117+
android:layout_marginTop="@dimen/standard_quarter_margin"
118+
android:background="@android:color/darker_gray"
119+
android:alpha="0.5"/>
120+
121+
</LinearLayout>

owncloudApp/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,9 @@
906906
<string name="member_type_user">User</string>
907907
<string name="member_type_group">Group</string>
908908
<string name="current_user_label">(me)</string>
909+
<string name="public_link_view">Can view</string>
910+
<string name="public_link_edit">Can edit</string>
911+
<string name="public_link_create_only">Secret file drop</string>
909912

910913
<string name="feedback_dialog_get_in_contact_description"><![CDATA[ Ask for help in our <a href=\"%1$s\"><b>forum</b></a> or contribute in our <a href=\"%2$s\"><b>GitHub repo</b></a>]]></string>
911914

0 commit comments

Comments
 (0)