Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
104d07c
feat: add members option in bottom sheet dialog
joragua Nov 28, 2025
54a76cc
feat: create new layout for space members
joragua Dec 10, 2025
c579dd9
feat: implement methods to fetch all space members information
joragua Dec 10, 2025
5d0443a
feat: display space members list in the new layout
joragua Dec 11, 2025
3db20a1
test: adapt existing tests to the new space permission response
joragua Dec 11, 2025
57eb6e7
test: add new tests for OCRemoteSpacesDataSourceTest and OCSpacesRepo…
joragua Dec 11, 2025
4fc2acf
feat: add release note
joragua Dec 11, 2025
d9dddd4
chore: add calens file
joragua Dec 12, 2025
f90bd84
refactor: use Locale.ROOT instead of default locale when displaying e…
joragua Dec 12, 2025
7ccd678
refactor: remove unnecessary XML elements and auxiliary method for ob…
joragua Dec 12, 2025
c0c86b1
feat: add support to fetch platform roles from server
joragua Dec 17, 2025
1689c48
feat: display correct role for each space member in the list
joragua Dec 17, 2025
39e2a06
test: create tests for OCRemoteRolesDataSource and OCRolesRepository
joragua Dec 17, 2025
ab3c816
docs: calens changelog updated
ownclouders Dec 17, 2025
65e343b
refactor: remove duplicated code from GetRemoteRolesOperation
joragua Dec 17, 2025
eb243ad
fix: initialize roles with empty list to prevent crashes in landscape
joragua Dec 17, 2025
3245c2f
fix: ellipsize member names in UI if too long
joragua Dec 17, 2025
cb10366
feat: improve content descriptions for space members (names and expir…
joragua Dec 17, 2025
02353fc
fix: observe roles flow before space members to avoid empty roles in …
joragua Dec 18, 2025
83785f2
fix: parse link response to show member option in all spaces
joragua Dec 18, 2025
9c06876
test: adapt existing test models to the new link response
joragua Dec 18, 2025
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ ownCloud admins and users.
## Summary

* Change - Migrate tests to the new kotlinx-coroutines-test API: [#4710](https://github.com/owncloud/android/issues/4710)
* Enhancement - Show members of a space: [#4612](https://github.com/owncloud/android/issues/4612)
* Enhancement - Set emoji as space image: [#4707](https://github.com/owncloud/android/issues/4707)

## Details
Expand All @@ -53,6 +54,16 @@ ownCloud admins and users.
https://github.com/owncloud/android/issues/4710
https://github.com/owncloud/android/pull/4722

* Enhancement - Show members of a space: [#4612](https://github.com/owncloud/android/issues/4612)

A new option to view all members of a space has been added to the bottom sheet,
available when the three-dot menu button is tapped. This new option opens a new
view that shows information like member names, roles, and expiration dates (if
available).

https://github.com/owncloud/android/issues/4612
https://github.com/owncloud/android/pull/4728

* Enhancement - Set emoji as space image: [#4707](https://github.com/owncloud/android/issues/4707)

A new option to set an emoji as space image has been added to the bottom sheet,
Expand Down
7 changes: 7 additions & 0 deletions changelog/unreleased/4728
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Show members of a space

A new option to view all members of a space has been added to the bottom sheet, available when the three-dot menu button is tapped.
This new option opens a new view that shows information like member names, roles, and expiration dates (if available).

https://github.com/owncloud/android/issues/4612
https://github.com/owncloud/android/pull/4728
6 changes: 6 additions & 0 deletions owncloudApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@
android:name="android.app.searchable"
android:resource="@xml/users_and_groups_searchable" />
</activity>
<activity
android:name=".presentation.spaces.members.SpaceMembersActivity"
android:exported="false"
android:label="@string/space_members_label"
android:launchMode="singleTop"
android:theme="@style/Theme.ownCloud.Toolbar"/>
<activity
android:name=".presentation.security.pattern.PatternActivity"
android:screenOrientation="portrait"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
* ownCloud Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
* @author Jorge Aguado Recio
*
* Copyright (C) 2025 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
Expand Down Expand Up @@ -33,6 +35,8 @@ import com.owncloud.android.data.files.datasources.RemoteFileDataSource
import com.owncloud.android.data.files.datasources.implementation.OCRemoteFileDataSource
import com.owncloud.android.data.oauth.datasources.RemoteOAuthDataSource
import com.owncloud.android.data.oauth.datasources.implementation.OCRemoteOAuthDataSource
import com.owncloud.android.data.roles.datasources.RemoteRolesDataSource
import com.owncloud.android.data.roles.datasources.implementation.OCRemoteRolesDataSource
import com.owncloud.android.data.server.datasources.RemoteServerInfoDataSource
import com.owncloud.android.data.server.datasources.implementation.OCRemoteServerInfoDataSource
import com.owncloud.android.data.sharing.sharees.datasources.RemoteShareeDataSource
Expand Down Expand Up @@ -73,6 +77,7 @@ val remoteDataSourceModule = module {
singleOf(::OCRemoteCapabilitiesDataSource) bind RemoteCapabilitiesDataSource::class
singleOf(::OCRemoteFileDataSource) bind RemoteFileDataSource::class
singleOf(::OCRemoteOAuthDataSource) bind RemoteOAuthDataSource::class
singleOf(::OCRemoteRolesDataSource) bind RemoteRolesDataSource::class
singleOf(::OCRemoteServerInfoDataSource) bind RemoteServerInfoDataSource::class
singleOf(::OCRemoteShareDataSource) bind RemoteShareDataSource::class
singleOf(::OCRemoteShareeDataSource) bind RemoteShareeDataSource::class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* @author David González Verdugo
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
* @author Jorge Aguado Recio
*
* Copyright (C) 2022 ownCloud GmbH.
* Copyright (C) 2025 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
Expand All @@ -28,6 +29,7 @@ import com.owncloud.android.data.capabilities.repository.OCCapabilityRepository
import com.owncloud.android.data.files.repository.OCFileRepository
import com.owncloud.android.data.folderbackup.repository.OCFolderBackupRepository
import com.owncloud.android.data.oauth.repository.OCOAuthRepository
import com.owncloud.android.data.roles.repository.OCRolesRepository
import com.owncloud.android.data.server.repository.OCServerInfoRepository
import com.owncloud.android.data.sharing.sharees.repository.OCShareeRepository
import com.owncloud.android.data.sharing.shares.repository.OCShareRepository
Expand All @@ -41,6 +43,7 @@ import com.owncloud.android.domain.authentication.oauth.OAuthRepository
import com.owncloud.android.domain.automaticuploads.FolderBackupRepository
import com.owncloud.android.domain.capabilities.CapabilityRepository
import com.owncloud.android.domain.files.FileRepository
import com.owncloud.android.domain.roles.RolesRepository
import com.owncloud.android.domain.server.ServerInfoRepository
import com.owncloud.android.domain.sharing.sharees.ShareeRepository
import com.owncloud.android.domain.sharing.shares.ShareRepository
Expand All @@ -59,6 +62,7 @@ val repositoryModule = module {
factoryOf(::OCFileRepository) bind FileRepository::class
factoryOf(::OCFolderBackupRepository) bind FolderBackupRepository::class
factoryOf(::OCOAuthRepository) bind OAuthRepository::class
factoryOf(::OCRolesRepository) bind RolesRepository::class
factoryOf(::OCServerInfoRepository) bind ServerInfoRepository::class
factoryOf(::OCShareRepository) bind ShareRepository::class
factoryOf(::OCShareeRepository) bind ShareeRepository::class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import com.owncloud.android.domain.files.usecases.SetLastUsageFileUseCase
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.roles.usecases.GetRolesAsyncUseCase
import com.owncloud.android.domain.server.usecases.GetServerInfoAsyncUseCase
import com.owncloud.android.domain.sharing.sharees.GetShareesAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.CreatePrivateShareAsyncUseCase
Expand All @@ -105,6 +106,7 @@ import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCa
import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase
import com.owncloud.android.domain.spaces.usecases.GetSpacesFromEveryAccountUseCaseAsStream
import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase
import com.owncloud.android.domain.spaces.usecases.GetSpaceMembersUseCase
import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransferByIdUseCase
import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase
import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsStreamUseCase
Expand Down Expand Up @@ -241,6 +243,7 @@ val useCaseModule = module {
factoryOf(::GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase)
factoryOf(::GetProjectSpacesWithSpecialsForAccountAsStreamUseCase)
factoryOf(::GetSpaceByIdForAccountUseCase)
factoryOf(::GetSpaceMembersUseCase)
factoryOf(::GetSpacePermissionsAsyncUseCase)
factoryOf(::GetSpaceWithSpecialsByIdForAccountUseCase)
factoryOf(::GetSpacesFromEveryAccountUseCaseAsStream)
Expand Down Expand Up @@ -300,4 +303,7 @@ val useCaseModule = module {

// Accounts
factoryOf(::RemoveAccountUseCase)

// Roles
factoryOf(::GetRolesAsyncUseCase)
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import com.owncloud.android.presentation.settings.more.SettingsMoreViewModel
import com.owncloud.android.presentation.settings.security.SettingsSecurityViewModel
import com.owncloud.android.presentation.sharing.ShareViewModel
import com.owncloud.android.presentation.spaces.SpacesListViewModel
import com.owncloud.android.presentation.spaces.members.SpaceMembersViewModel
import com.owncloud.android.presentation.transfers.TransfersViewModel
import com.owncloud.android.ui.ReceiveExternalFilesViewModel
import com.owncloud.android.ui.preview.PreviewImageViewModel
Expand Down Expand Up @@ -84,6 +85,7 @@ val viewModelModule = module {
viewModelOf(::SettingsSecurityViewModel)
viewModelOf(::SettingsVideoUploadsViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::SpaceMembersViewModel)
viewModelOf(::FileOperationsViewModel)

viewModel { (accountName: String) -> CapabilityViewModel(accountName, get(), get(), get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fun SpaceMenuOption.toStringResId() =
SpaceMenuOption.ENABLE -> R.string.enable_space
SpaceMenuOption.DELETE -> R.string.delete_space
SpaceMenuOption.SET_ICON -> R.string.set_space_icon
SpaceMenuOption.MEMBERS -> R.string.space_members
}

fun SpaceMenuOption.toDrawableResId() =
Expand All @@ -41,4 +42,5 @@ fun SpaceMenuOption.toDrawableResId() =
SpaceMenuOption.ENABLE -> R.drawable.ic_enable_space
SpaceMenuOption.DELETE -> R.drawable.ic_action_delete_white
SpaceMenuOption.SET_ICON -> R.drawable.ic_set_space_icon
SpaceMenuOption.MEMBERS -> R.drawable.ic_share_generic_white
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ class ReleaseNotesViewModel(

companion object {
val releaseNotesList = listOf(
ReleaseNote(
title = R.string.release_notes_4_8_0_title_space_membership,
subtitle = R.string.release_notes_4_8_0_subtitle_space_membership,
type = ReleaseNoteType.ENHANCEMENT
),
ReleaseNote(
title = R.string.release_notes_4_8_0_title_set_emoji_as_space_image,
subtitle = R.string.release_notes_4_8_0_subtitle_set_emoji_as_space_image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import com.owncloud.android.presentation.common.BottomSheetFragmentItemView
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.presentation.common.UIResult
import com.owncloud.android.presentation.spaces.createspace.CreateSpaceDialogFragment
import com.owncloud.android.presentation.spaces.members.SpaceMembersActivity
import com.owncloud.android.presentation.transfers.TransfersViewModel
import com.owncloud.android.presentation.spaces.setspaceicon.SetSpaceIconDialogFragment
import kotlinx.coroutines.flow.SharedFlow
Expand Down Expand Up @@ -461,6 +462,11 @@ class SpacesListFragment :
val setIconDialog = SetSpaceIconDialogFragment.newInstance(listener = this@SpacesListFragment)
setIconDialog.show(requireActivity().supportFragmentManager, DIALOG_SET_ICON)
}
SpaceMenuOption.MEMBERS -> {
val intent = Intent(requireActivity(), SpaceMembersActivity::class.java)
intent.putExtra(SpaceMembersActivity.EXTRA_SPACE, currentSpace)
startActivity(intent)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* ownCloud Android client application
*
* @author Jorge Aguado Recio
*
* Copyright (C) 2025 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.owncloud.android.presentation.spaces.members

import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.fragment.app.transaction
import com.owncloud.android.R
import com.owncloud.android.domain.spaces.model.OCSpace
import com.owncloud.android.ui.activity.FileActivity

class SpaceMembersActivity: FileActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(R.layout.members_activity)

setupStandardToolbar(title = null, displayHomeAsUpEnabled = true, homeButtonEnabled = true, displayShowTitleEnabled = true)

supportActionBar?.setHomeActionContentDescription(R.string.common_back)

val currentSpace = intent.getParcelableExtra<OCSpace>(EXTRA_SPACE)

supportFragmentManager.transaction {
if (savedInstanceState == null && currentSpace != null) {
val fragment = SpaceMembersFragment.newInstance(account.name, currentSpace)
replace(R.id.members_fragment_container, fragment, TAG_SPACE_MEMBERS_FRAGMENT)
}
}
}

override fun onCreateOptionsMenu(menu: Menu): Boolean = false

override fun onOptionsItemSelected(item: MenuItem): Boolean =
if (item.itemId == android.R.id.home && !supportFragmentManager.popBackStackImmediate()) {
finish()
true
} else {
super.onOptionsItemSelected(item)
}

companion object {
private const val TAG_SPACE_MEMBERS_FRAGMENT = "SPACE_MEMBERS_FRAGMENT"
const val EXTRA_SPACE = "EXTRA_SPACE"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* ownCloud Android client application
*
* @author Jorge Aguado Recio
*
* Copyright (C) 2025 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.owncloud.android.presentation.spaces.members

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.owncloud.android.R
import com.owncloud.android.databinding.MemberItemBinding
import com.owncloud.android.domain.roles.model.OCRole
import com.owncloud.android.domain.spaces.model.SpaceMember
import com.owncloud.android.domain.spaces.model.SpaceMembers
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.PreferenceUtils

class SpaceMembersAdapter: RecyclerView.Adapter<SpaceMembersAdapter.SpaceMembersViewHolder>() {

private var members: List<SpaceMember> = emptyList()
private var rolesMap: Map<String, String> = emptyMap()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SpaceMembersViewHolder {
val inflater = LayoutInflater.from(parent.context)

val view = inflater.inflate(R.layout.member_item, parent, false)
view.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(parent.context)

return SpaceMembersViewHolder(view)
}

override fun onBindViewHolder(holder: SpaceMembersViewHolder, position: Int) {
val member = members[position]
val roleNames = member.roles.mapNotNull { rolesMap[it] }

holder.binding.apply {
val isGroup = member.id.startsWith(GROUP_PREFIX)
memberIcon.setImageResource(if (isGroup) R.drawable.ic_group else R.drawable.ic_user)
memberName.text = member.displayName
memberName.contentDescription = holder.itemView.context.getString(
if (isGroup) R.string.content_description_member_group else R.string.content_description_member_user, member.displayName
)
memberRole.text = roleNames.joinToString(", ")

member.expirationDateTime?.let {
expirationCalendarIcon.visibility = View.VISIBLE
expirationDate.visibility = View.VISIBLE
expirationDate.text = DisplayUtils.displayDateToHumanReadable(it)
expirationDate.contentDescription =
holder.itemView.context.getString(R.string.content_description_member_expiration_date, expirationDate.text)
}
}
}

override fun getItemCount(): Int = members.size

fun setSpaceMembers(spaceMembers: SpaceMembers, roles: List<OCRole>) {
this.rolesMap = roles.associate { it.id to it.displayName }
this.members = spaceMembers.members.sortedByDescending { member -> roles.indexOfFirst { it.id in member.roles } }
notifyDataSetChanged()
}

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

companion object {
const val GROUP_PREFIX = "g:"
}
}
Loading
Loading