diff --git a/AGENTS.md b/AGENTS.md
index 62d0a7127e8e..17b7622f8673 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -93,8 +93,8 @@ Avoid creating source files that implement multiple types; instead, place each t
### Jetpack Compose
Modern UI is built with Jetpack Compose (Material 3). Key directories:
-- `com.nextcloud.ui.composeActivity` - Activities hosting Compose content
-- `com.nextcloud.ui.composeComponents` - Reusable Compose components
+- `com.nextcloud.ui.compose` - Activities hosting Compose content
+- `com.nextcloud.ui.compose.component` - Reusable Compose components
- `com.nextcloud.client.assistant` - Compose-based Assistant screens and features
Use `StateFlow` and `MutableStateFlow` in ViewModels for state management. Collect state in Compose functions with `collectAsState()`.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8ad94b13f8e8..5c3eef6ac574 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -405,6 +405,14 @@ dependencies {
implementation(libs.lifecycle.process)
implementation(libs.lifecycle.service)
implementation(libs.lifecycle.runtime.ktx)
+ implementation(libs.lifecycle.runtime.compose)
+ implementation(libs.lifecycle.viewmodel.compose)
+ implementation(libs.lifecycle.viewmodel.navigation3)
+ // endregion
+
+ // region Navigation 3
+ implementation(libs.navigation3.runtime)
+ implementation(libs.navigation3.ui)
// endregion
// region JUnit
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8014d33ba07c..6bf5c727d1a0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -132,13 +132,13 @@
android:resource="@xml/app_config" />
@@ -340,6 +340,10 @@
android:name=".ui.activity.TextEditorWebView"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="false" />
+
{
- SimpleAlertDialog(
+ BaseAlertDialog(
title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title),
- description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description),
onDismiss = { viewModel.updateScreenOverlayState(null) },
- onComplete = { viewModel.deleteTask(state.id) }
- )
+ onComplete = {
+ viewModel.deleteTask(state.id)
+ viewModel.updateScreenOverlayState(null)
+ }
+ ) {
+ Text(stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description))
+ }
}
is ScreenOverlayState.TaskActions -> {
@@ -524,6 +530,7 @@ private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, description
}
@Suppress("MagicNumber")
+@SuppressLint("ViewModelConstructorInComposable")
@Composable
@Preview
private fun AssistantScreenPreview() {
@@ -544,6 +551,7 @@ private fun AssistantScreenPreview() {
}
@Suppress("MagicNumber")
+@SuppressLint("ViewModelConstructorInComposable")
@Composable
@Preview
private fun AssistantEmptyScreenPreview() {
diff --git a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/TaskSelectionAlertDialog.kt b/app/src/main/java/com/nextcloud/client/assistant/component/TaskSelectionAlertDialog.kt
similarity index 97%
rename from app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/TaskSelectionAlertDialog.kt
rename to app/src/main/java/com/nextcloud/client/assistant/component/TaskSelectionAlertDialog.kt
index 9cae1dd238fb..ddffcd56da1e 100644
--- a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/TaskSelectionAlertDialog.kt
+++ b/app/src/main/java/com/nextcloud/client/assistant/component/TaskSelectionAlertDialog.kt
@@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2026 Alper Ozturk
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-package com.nextcloud.ui.composeComponents.alertDialog
+package com.nextcloud.client.assistant.component
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt
index 8d58a5d32b3e..4e820601112b 100644
--- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt
+++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt
@@ -53,7 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.nextcloud.client.assistant.conversation.model.ConversationScreenState
-import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet
+import com.nextcloud.ui.compose.component.bottomSheet.MoreActionsBottomSheet
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.chat.model.Conversation
diff --git a/app/src/main/java/com/nextcloud/client/di/AppModule.kt b/app/src/main/java/com/nextcloud/client/di/AppModule.kt
index 8154074c608d..d55dbd4ce7f7 100644
--- a/app/src/main/java/com/nextcloud/client/di/AppModule.kt
+++ b/app/src/main/java/com/nextcloud/client/di/AppModule.kt
@@ -44,6 +44,10 @@ import com.nextcloud.client.notifications.AppNotificationManagerImpl
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.repository.ClientRepository
import com.nextcloud.repository.RemoteClientRepository
+import com.nextcloud.ui.teams.detail.repository.TeamDetailRemoteRepository
+import com.nextcloud.ui.teams.detail.repository.TeamDetailRepository
+import com.nextcloud.ui.teams.repository.TeamsRemoteRepository
+import com.nextcloud.ui.teams.repository.TeamsRepository
import com.nextcloud.utils.e2ee.E2EEActionResolver
import com.nextcloud.utils.e2ee.E2EEKeyInspector
import com.nextcloud.utils.thumbnail.FolderThumbnailGenerator
@@ -119,6 +123,12 @@ internal class AppModule {
@Provides
fun clientRepository(repository: RemoteClientRepository): ClientRepository = repository
+ @Provides
+ fun teamsRepository(repository: TeamsRemoteRepository): TeamsRepository = repository
+
+ @Provides
+ fun teamDetailRepository(repository: TeamDetailRemoteRepository): TeamDetailRepository = repository
+
@Provides
fun fileUploadHelper(): FileUploadHelper = FileUploadHelper.instance()
diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt
index 745696d45571..3182bdce5ab9 100644
--- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt
+++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt
@@ -28,7 +28,7 @@ import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity
import com.nextcloud.client.widget.DashboardWidgetProvider
import com.nextcloud.client.widget.DashboardWidgetService
import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet
-import com.nextcloud.ui.composeActivity.ComposeActivity
+import com.nextcloud.ui.compose.ComposeActivity
import com.nextcloud.ui.dialog.account.ChooseAccountDialogFragment
import com.nextcloud.ui.dialog.account.onlineStatus.SetOnlineStatusBottomSheet
import com.nextcloud.ui.dialog.account.statusMessage.SetStatusMessageBottomSheet
@@ -36,6 +36,7 @@ import com.nextcloud.ui.dialog.chooseStorage.ChooseStorageLocationDialogFragment
import com.nextcloud.ui.fileInfo.FileInfoFragment
import com.nextcloud.ui.fileactions.FileActionsBottomSheet
import com.nextcloud.ui.tags.TagManagementBottomSheet
+import com.nextcloud.ui.teams.detail.TeamWebView
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet
import com.nmc.android.ui.LauncherActivity
import com.owncloud.android.MainApp
@@ -253,6 +254,9 @@ internal interface ComponentsModule {
@ContributesAndroidInjector
fun textEditorWebView(): TextEditorWebView
+ @ContributesAndroidInjector
+ fun teamWebView(): TeamWebView
+
@ContributesAndroidInjector
fun extendedListFragment(): ExtendedListFragment
diff --git a/app/src/main/java/com/nextcloud/client/di/SavedStateViewModelCreator.kt b/app/src/main/java/com/nextcloud/client/di/SavedStateViewModelCreator.kt
new file mode 100644
index 000000000000..d4170bcc42e5
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/client/di/SavedStateViewModelCreator.kt
@@ -0,0 +1,14 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.client.di
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+
+fun interface SavedStateViewModelCreator {
+ fun create(savedStateHandle: SavedStateHandle): ViewModel
+}
diff --git a/app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt
index 08041ae60feb..ad40629affd6 100644
--- a/app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt
+++ b/app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt
@@ -8,6 +8,8 @@ package com.nextcloud.client.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.createSavedStateHandle
+import androidx.lifecycle.viewmodel.CreationExtras
import javax.inject.Inject
import javax.inject.Provider
@@ -21,9 +23,17 @@ import javax.inject.Provider
* @see ViewModelKey
*/
class ViewModelFactory @Inject constructor(
- private val viewModelProviders: Map, @JvmSuppressWildcards Provider>
+ private val viewModelProviders: Map, @JvmSuppressWildcards Provider>,
+ private val savedStateViewModelCreators: Map, @JvmSuppressWildcards SavedStateViewModelCreator>
) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class, extras: CreationExtras): T {
+ val creator = savedStateViewModelCreators[modelClass] ?: return create(modelClass)
+
+ @Suppress("UNCHECKED_CAST")
+ return creator.create(extras.createSavedStateHandle()) as T
+ }
+
override fun create(modelClass: Class): T {
var vmProvider: Provider? = viewModelProviders[modelClass]
diff --git a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
index 0dc9da9cb15f..162bba10cf30 100644
--- a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
+++ b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
@@ -16,6 +16,8 @@ import com.nextcloud.client.logger.ui.LogsViewModel
import com.nextcloud.client.player.ui.audio.AudioPlayerViewModel
import com.nextcloud.ui.fileactions.FileActionsViewModel
import com.nextcloud.ui.tags.TagManagementViewModel
+import com.nextcloud.ui.teams.detail.viewmodel.TeamDetailViewModel
+import com.nextcloud.ui.teams.viewmodel.TeamsViewModel
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsViewModel
import com.owncloud.android.ui.activity.filedisplayactivity.FileDisplayActivityViewModel
import com.owncloud.android.ui.fragment.filedetail.FileDetailFragmentViewModel
@@ -84,4 +86,14 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(FileDetailFragmentViewModel::class)
abstract fun fileDetailFragmentViewModel(vm: FileDetailFragmentViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(TeamsViewModel::class)
+ abstract fun teamsViewModel(factory: TeamsViewModel.Factory): SavedStateViewModelCreator
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(TeamDetailViewModel::class)
+ abstract fun teamDetailViewModel(factory: TeamDetailViewModel.Factory): SavedStateViewModelCreator
}
diff --git a/app/src/main/java/com/nextcloud/ui/compose/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/compose/ComposeActivity.kt
new file mode 100644
index 000000000000..b0b1ffa78a1c
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/compose/ComposeActivity.kt
@@ -0,0 +1,131 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.compose
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.activity.viewModels
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.core.view.isVisible
+import com.nextcloud.client.di.ViewModelFactory
+import com.nextcloud.ui.compose.navigation.ComposeDestination
+import com.nextcloud.ui.compose.navigation.ComposeNavHost
+import com.nextcloud.ui.compose.navigation.rememberComposeNavigator
+import com.nextcloud.utils.extensions.getParcelableArgument
+import com.owncloud.android.R
+import com.owncloud.android.databinding.ActivityComposeBinding
+import com.owncloud.android.ui.activity.drawer.DrawerActivity
+import javax.inject.Inject
+
+class ComposeActivity : DrawerActivity() {
+
+ @Inject
+ lateinit var vmFactory: ViewModelFactory
+
+ lateinit var binding: ActivityComposeBinding
+ val composeViewModel: ComposeViewModel by viewModels()
+
+ private val destination: ComposeDestination by lazy {
+ intent.getParcelableArgument(DESTINATION, ComposeDestination::class.java)
+ ?: ComposeDestination.getAssistantScreen(this)
+ }
+
+ companion object {
+ const val DESTINATION = "DESTINATION"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityComposeBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setupActivityUIFor(destination)
+
+ binding.composeView.setContent {
+ MaterialTheme(
+ colorScheme = viewThemeUtils.getColorScheme(this),
+ content = {
+ Content()
+ }
+ )
+ }
+
+ processText(intent)
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ processText(intent)
+ }
+
+ private fun processText(intent: Intent) {
+ val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
+ if (text.isNullOrEmpty()) {
+ return
+ }
+
+ composeViewModel.updateSelectedText(text.toString())
+ }
+
+ override val menuItemId: Int
+ get() = if (destination is ComposeDestination.TeamsScreen) Menu.NONE else R.id.nav_assistant
+
+ override fun onResume() {
+ super.onResume()
+ highlightNavigationViewItem(menuItemId)
+ }
+
+ private fun setupActivityUIFor(destination: ComposeDestination) {
+ when (destination) {
+ is ComposeDestination.AssistantScreen -> setupDrawerScreen(destination.title)
+ is ComposeDestination.TeamsScreen -> setupDrawerScreen(destination.title)
+ else -> setSupportActionBar(null)
+ }
+ }
+
+ private fun setupDrawerScreen(title: String) {
+ setupDrawer(menuItemId)
+ showMenuAndTitleActionBar(title) {
+ openDrawer()
+ }
+ }
+
+ private fun showActivityBarsFor(screen: ComposeDestination) {
+ val isDrawerScreen = screen is ComposeDestination.AssistantScreen || screen is ComposeDestination.TeamsScreen
+ findViewById(R.id.appbar)?.isVisible = isDrawerScreen
+ binding.bottomNavigation.isVisible = isDrawerScreen
+
+ if (screen is ComposeDestination.AssistantScreen) {
+ binding.bottomNavigation.menu.findItem(R.id.nav_assistant)?.isChecked = true
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
+ android.R.id.home -> {
+ toggleDrawer()
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ @Composable
+ private fun Content() {
+ val navigator = rememberComposeNavigator(destination, onExit = ::finish)
+
+ LaunchedEffect(navigator.current) {
+ showActivityBarsFor(navigator.current)
+ }
+
+ ComposeNavHost(navigator)
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeProcessTextAlias.kt b/app/src/main/java/com/nextcloud/ui/compose/ComposeProcessTextAlias.kt
similarity index 90%
rename from app/src/main/java/com/nextcloud/ui/composeActivity/ComposeProcessTextAlias.kt
rename to app/src/main/java/com/nextcloud/ui/compose/ComposeProcessTextAlias.kt
index 5b280dcc9a02..a2c70d62c52e 100644
--- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeProcessTextAlias.kt
+++ b/app/src/main/java/com/nextcloud/ui/compose/ComposeProcessTextAlias.kt
@@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2026 Alper Ozturk
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-package com.nextcloud.ui.composeActivity
+package com.nextcloud.ui.compose
import android.content.ComponentName
import android.content.Context
@@ -22,7 +22,7 @@ class ComposeProcessTextAlias @Inject constructor(private val context: Context)
val componentName = ComponentName(
context,
- "com.nextcloud.ui.composeActivity.ComposeProcessTextAlias"
+ "com.nextcloud.ui.compose.ComposeProcessTextAlias"
)
val newState = if (isAssistantAvailable) {
diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeViewModel.kt b/app/src/main/java/com/nextcloud/ui/compose/ComposeViewModel.kt
similarity index 93%
rename from app/src/main/java/com/nextcloud/ui/composeActivity/ComposeViewModel.kt
rename to app/src/main/java/com/nextcloud/ui/compose/ComposeViewModel.kt
index 86b94a633cd8..4571b7c8606d 100644
--- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeViewModel.kt
+++ b/app/src/main/java/com/nextcloud/ui/compose/ComposeViewModel.kt
@@ -5,7 +5,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-package com.nextcloud.ui.composeActivity
+package com.nextcloud.ui.compose
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
diff --git a/app/src/main/java/com/nextcloud/ui/compose/component/alertDialog/BaseAlertDialog.kt b/app/src/main/java/com/nextcloud/ui/compose/component/alertDialog/BaseAlertDialog.kt
new file mode 100644
index 000000000000..046438a24d9d
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/compose/component/alertDialog/BaseAlertDialog.kt
@@ -0,0 +1,108 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.compose.component.alertDialog
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.AlertDialogDefaults
+import androidx.compose.material3.BasicAlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.owncloud.android.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BaseAlertDialog(
+ title: String,
+ onDismiss: () -> Unit,
+ onComplete: (() -> Unit)? = null,
+ completeText: String = stringResource(R.string.common_ok),
+ isCompleteEnabled: Boolean = true,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ // Previews cannot render the separate window opened by BasicAlertDialog, so draw the card inline there
+ if (LocalInspectionMode.current) {
+ BaseAlertDialogContent(title, onDismiss, onComplete, completeText, isCompleteEnabled, content)
+ return
+ }
+
+ BasicAlertDialog(onDismissRequest = onDismiss) {
+ BaseAlertDialogContent(title, onDismiss, onComplete, completeText, isCompleteEnabled, content)
+ }
+}
+
+@Composable
+private fun BaseAlertDialogContent(
+ title: String,
+ onDismiss: () -> Unit,
+ onComplete: (() -> Unit)?,
+ completeText: String,
+ isCompleteEnabled: Boolean,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ Surface(
+ shape = AlertDialogDefaults.shape,
+ color = AlertDialogDefaults.containerColor,
+ tonalElevation = AlertDialogDefaults.TonalElevation
+ ) {
+ Column(
+ modifier = Modifier.padding(dimensionResource(R.dimen.dialog_padding)),
+ verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.standard_padding))
+ ) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineSmall,
+ color = AlertDialogDefaults.titleContentColor
+ )
+ content()
+ Row(
+ modifier = Modifier.align(Alignment.End),
+ horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.standard_half_padding))
+ ) {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.common_cancel))
+ }
+ onComplete?.let {
+ FilledTonalButton(onClick = it, enabled = isCompleteEnabled) {
+ Text(completeText)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun BaseAlertDialogPreview() {
+ MaterialTheme {
+ BaseAlertDialog(
+ title = "Title",
+ onDismiss = {},
+ onComplete = {}
+ ) {
+ Text(
+ "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the" +
+ " industry's standard dummy text ever since 1966, when designers at Letraset and James Mosley, the"
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/compose/component/bottomSheet/MoreActionsBottomSheet.kt
similarity index 98%
rename from app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt
rename to app/src/main/java/com/nextcloud/ui/compose/component/bottomSheet/MoreActionsBottomSheet.kt
index 94dd7c39b9b6..3fccf5971400 100644
--- a/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt
+++ b/app/src/main/java/com/nextcloud/ui/compose/component/bottomSheet/MoreActionsBottomSheet.kt
@@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2024 Alper Ozturk
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
-package com.nextcloud.ui.composeComponents.bottomSheet
+package com.nextcloud.ui.compose.component.bottomSheet
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt b/app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeDestination.kt
similarity index 63%
rename from app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt
rename to app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeDestination.kt
index 6564d61cd515..a5b387f99004 100644
--- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt
+++ b/app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeDestination.kt
@@ -5,7 +5,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
-package com.nextcloud.ui.composeActivity
+package com.nextcloud.ui.compose.navigation
import android.content.Context
import android.os.Parcelable
@@ -14,14 +14,22 @@ import com.owncloud.android.R
import kotlinx.parcelize.Parcelize
@Parcelize
-sealed class ComposeDestination(val id: Int) : Parcelable {
+sealed class ComposeDestination : Parcelable {
@Parcelize
- data class AssistantScreen(val title: String, val sessionId: Long?) : ComposeDestination(0)
+ data class AssistantScreen(val title: String, val sessionId: Long?) : ComposeDestination()
@Parcelize
- data class ClientIntegrationScreen(val title: String, val data: ClientIntegrationUI) : ComposeDestination(1)
+ data class ClientIntegrationScreen(val title: String, val data: ClientIntegrationUI) : ComposeDestination()
+
+ @Parcelize
+ data class TeamsScreen(val title: String) : ComposeDestination()
+
+ @Parcelize
+ data class TeamDetailScreen(val teamId: String, val title: String, val settingsUrl: String) : ComposeDestination()
companion object {
+ fun getTeamsScreen(context: Context): TeamsScreen = TeamsScreen(context.getString(R.string.navigation_teams))
+
/**
* Creates a assistant screen without selected chat
*/
diff --git a/app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeNavHost.kt b/app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeNavHost.kt
new file mode 100644
index 000000000000..203afc5f8eea
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeNavHost.kt
@@ -0,0 +1,152 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.compose.navigation
+
+import android.os.Bundle
+import androidx.activity.compose.LocalActivity
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.lifecycle.DEFAULT_ARGS_KEY
+import androidx.lifecycle.HasDefaultViewModelProviderFactory
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.lifecycle.viewmodel.MutableCreationExtras
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
+import androidx.navigation3.runtime.entryProvider
+import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
+import androidx.navigation3.ui.NavDisplay
+import com.nextcloud.client.assistant.AssistantScreen
+import com.nextcloud.client.assistant.AssistantViewModel
+import com.nextcloud.client.assistant.chat.ChatViewModel
+import com.nextcloud.client.assistant.conversation.ConversationViewModel
+import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepositoryImpl
+import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryImpl
+import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl
+import com.nextcloud.client.database.NextcloudDatabase
+import com.nextcloud.client.di.ViewModelFactory
+import com.nextcloud.common.NextcloudClient
+import com.nextcloud.ui.ClientIntegrationScreen
+import com.nextcloud.ui.compose.ComposeActivity
+import com.nextcloud.ui.teams.TeamsScreen
+import com.nextcloud.ui.teams.detail.TeamDetailScreen
+import com.nextcloud.ui.teams.detail.TeamResourceOpener
+import com.nextcloud.ui.teams.detail.model.TeamResourceTarget
+import com.nextcloud.ui.teams.detail.viewmodel.TeamDetailViewModel
+import com.nextcloud.ui.teams.viewmodel.TeamsViewModel
+import com.nextcloud.utils.SnackbarUtil
+import com.owncloud.android.R
+import kotlin.jvm.optionals.getOrNull
+
+private val noTransition = ContentTransform(EnterTransition.None, ExitTransition.None, sizeTransform = null)
+
+@Composable
+fun ComposeNavHost(navigator: ComposeNavigator) {
+ NavDisplay(
+ backStack = navigator.backStack,
+ onBack = navigator::goBack,
+ entryDecorators = listOf(
+ rememberSaveableStateHolderNavEntryDecorator(),
+ rememberViewModelStoreNavEntryDecorator()
+ ),
+ transitionSpec = { noTransition },
+ popTransitionSpec = { noTransition },
+ predictivePopTransitionSpec = { noTransition },
+ entryProvider = entryProvider {
+ entry { screen ->
+ val activity = composeActivity()
+ val capability = activity.capabilities.getOrNull() ?: return@entry
+ val client = rememberNextcloudClient() ?: return@entry
+ val remoteRepository = remember(client, capability) {
+ AssistantRemoteRepositoryImpl(client, capability)
+ }
+
+ AssistantScreen(
+ composeViewModel = activity.composeViewModel,
+ viewModel = viewModel {
+ AssistantViewModel(
+ accountName = activity.userAccountManager.user.accountName,
+ remoteRepository = remoteRepository,
+ localRepository = AssistantLocalRepositoryImpl(NextcloudDatabase.instance().assistantDao()),
+ sessionIdArg = screen.sessionId
+ )
+ },
+ chatViewModel = viewModel { ChatViewModel(remoteRepository) },
+ conversationViewModel = viewModel {
+ ConversationViewModel(remoteRepository = ConversationRemoteRepositoryImpl(client))
+ },
+ activity = activity,
+ capability = capability
+ )
+ }
+
+ entry { screen ->
+ val client = rememberNextcloudClient() ?: return@entry
+
+ ClientIntegrationScreen(
+ clientIntegrationUI = screen.data,
+ baseUrl = client.baseUri.toString()
+ )
+ }
+
+ entry {
+ TeamsScreen(
+ viewModel = viewModel(factory = composeActivity().vmFactory),
+ onTeamClick = { team ->
+ navigator.navigate(ComposeDestination.TeamDetailScreen(team.id, team.displayName, team.url))
+ }
+ )
+ }
+
+ entry { screen ->
+ val activity = composeActivity()
+ val resourceOpener = remember(activity) {
+ TeamResourceOpener(activity, activity.userAccountManager.user)
+ }
+
+ TeamDetailScreen(
+ viewModel = teamDetailViewModel(activity.vmFactory, screen.teamId),
+ title = screen.title,
+ onBack = navigator::goBack,
+ onOpenSettings = {
+ resourceOpener.open(TeamResourceTarget.WebPage(screen.settingsUrl, screen.title))
+ },
+ onOpenResource = resourceOpener::open
+ )
+ }
+ }
+ )
+}
+
+@Composable
+private fun composeActivity(): ComposeActivity = LocalActivity.current as ComposeActivity
+
+@Composable
+private fun rememberNextcloudClient(): NextcloudClient? {
+ val activity = composeActivity()
+ val client by produceState(initialValue = null, activity) {
+ value = activity.clientRepository.getNextcloudClient()
+ if (value == null) {
+ SnackbarUtil.show(activity, R.string.account_not_found)
+ }
+ }
+ return client
+}
+
+@Composable
+private fun teamDetailViewModel(vmFactory: ViewModelFactory, teamId: String): TeamDetailViewModel {
+ val owner = LocalViewModelStoreOwner.current as? HasDefaultViewModelProviderFactory
+ val extras = MutableCreationExtras(owner?.defaultViewModelCreationExtras ?: CreationExtras.Empty).apply {
+ set(DEFAULT_ARGS_KEY, Bundle().apply { putString(TeamDetailViewModel.KEY_TEAM_ID, teamId) })
+ }
+ return viewModel(factory = vmFactory, extras = extras)
+}
diff --git a/app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeNavigator.kt b/app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeNavigator.kt
new file mode 100644
index 000000000000..837de59d08a7
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/compose/navigation/ComposeNavigator.kt
@@ -0,0 +1,50 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.compose.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.runtime.toMutableStateList
+
+class ComposeNavigator(private val entries: SnapshotStateList, private val onExit: () -> Unit) {
+
+ val backStack: List
+ get() = entries
+
+ val current: ComposeDestination
+ get() = entries.last()
+
+ fun navigate(destination: ComposeDestination) {
+ entries.add(destination)
+ }
+
+ fun goBack() {
+ if (entries.size > 1) {
+ entries.removeLastOrNull()
+ } else {
+ onExit()
+ }
+ }
+}
+
+private val backStackSaver = listSaver, ComposeDestination>(
+ save = { it.toList() },
+ restore = { it.toMutableStateList() }
+)
+
+@Composable
+fun rememberComposeNavigator(startDestination: ComposeDestination, onExit: () -> Unit): ComposeNavigator {
+ val entries = rememberSaveable(saver = backStackSaver) { mutableStateListOf(startDestination) }
+ val currentOnExit by rememberUpdatedState(onExit)
+ return remember(entries) { ComposeNavigator(entries) { currentOnExit() } }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt
deleted file mode 100644
index 3251a30cb908..000000000000
--- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Nextcloud - Android Client
- *
- * SPDX-FileCopyrightText: 2024 Alper Ozturk
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH
- * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
- */
-package com.nextcloud.ui.composeActivity
-
-import android.content.Intent
-import android.os.Bundle
-import android.view.MenuItem
-import android.view.View
-import androidx.activity.viewModels
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import com.nextcloud.client.assistant.AssistantScreen
-import com.nextcloud.client.assistant.AssistantViewModel
-import com.nextcloud.client.assistant.chat.ChatViewModel
-import com.nextcloud.client.assistant.conversation.ConversationViewModel
-import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepositoryImpl
-import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryImpl
-import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl
-import com.nextcloud.client.database.NextcloudDatabase
-import com.nextcloud.common.NextcloudClient
-import com.nextcloud.ui.ClientIntegrationScreen
-import com.nextcloud.utils.extensions.getParcelableArgument
-import com.owncloud.android.R
-import com.owncloud.android.databinding.ActivityComposeBinding
-import com.owncloud.android.ui.activity.drawer.DrawerActivity
-
-class ComposeActivity : DrawerActivity() {
-
- lateinit var binding: ActivityComposeBinding
- private val composeViewModel: ComposeViewModel by viewModels()
-
- companion object {
- const val DESTINATION = "DESTINATION"
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityComposeBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- val destination =
- intent.getParcelableArgument(DESTINATION, ComposeDestination::class.java)
- ?: ComposeDestination.getAssistantScreen(this)
-
- setupActivityUIFor(destination)
-
- binding.composeView.setContent {
- MaterialTheme(
- colorScheme = viewThemeUtils.getColorScheme(this),
- content = {
- Content(destination)
- }
- )
- }
-
- processText(intent)
- }
-
- override fun onNewIntent(intent: Intent) {
- super.onNewIntent(intent)
- processText(intent)
- }
-
- private fun processText(intent: Intent) {
- val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
- if (text.isNullOrEmpty()) {
- return
- }
-
- composeViewModel.updateSelectedText(text.toString())
- }
-
- override val menuItemId: Int
- get() = R.id.nav_assistant
-
- override fun onResume() {
- super.onResume()
- highlightNavigationViewItem(menuItemId)
- }
-
- private fun setupActivityUIFor(destination: ComposeDestination) {
- if (destination is ComposeDestination.AssistantScreen) {
- setupDrawer(menuItemId)
- showMenuAndTitleActionBar(destination.title) {
- openDrawer()
- }
- } else {
- setSupportActionBar(null)
- findViewById(R.id.appbar)?.let {
- it.visibility = View.GONE
- }
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
- android.R.id.home -> {
- toggleDrawer()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-
- @Composable
- private fun Content(destination: ComposeDestination) {
- val currentScreen by ComposeNavigation.currentScreen.collectAsState()
- var nextcloudClient by remember { mutableStateOf(null) }
-
- LaunchedEffect(Unit) {
- ComposeNavigation.navigate(destination)
- nextcloudClient = clientRepository.getNextcloudClient()
- }
-
- binding.bottomNavigation.menu.findItem(R.id.nav_assistant).run {
- isChecked = true
- }
-
- when (currentScreen) {
- is ComposeDestination.AssistantScreen -> {
- val dao = NextcloudDatabase.instance().assistantDao()
- val sessionId = (currentScreen as? ComposeDestination.AssistantScreen)?.sessionId
- val client = nextcloudClient ?: return
- val optionalCapability = capabilities
- if (optionalCapability.isEmpty) {
- return
- }
- val capability = optionalCapability.get()
- val remoteRepository = AssistantRemoteRepositoryImpl(client, capability)
-
- AssistantScreen(
- composeViewModel = composeViewModel,
- viewModel = AssistantViewModel(
- accountName = userAccountManager.user.accountName,
- remoteRepository = remoteRepository,
- localRepository = AssistantLocalRepositoryImpl(dao),
- sessionIdArg = sessionId
- ),
- chatViewModel = ChatViewModel(remoteRepository),
- conversationViewModel = ConversationViewModel(
- remoteRepository = ConversationRemoteRepositoryImpl(client)
- ),
- activity = this,
- capability = capability
- )
- }
-
- is ComposeDestination.ClientIntegrationScreen -> {
- binding.bottomNavigation.visibility = View.GONE
- val integrationScreen = (currentScreen as ComposeDestination.ClientIntegrationScreen)
- ClientIntegrationScreen(integrationScreen.data, nextcloudClient?.baseUri.toString())
- }
-
- else -> Unit
- }
- }
-}
diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeNavigation.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeNavigation.kt
deleted file mode 100644
index 453bb439b8f2..000000000000
--- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeNavigation.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Nextcloud - Android Client
- *
- * SPDX-FileCopyrightText: 2025 Alper Ozturk
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-package com.nextcloud.ui.composeActivity
-
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.update
-
-object ComposeNavigation {
- private var root: MutableStateFlow = MutableStateFlow(null)
- val currentScreen: StateFlow = root
- fun navigate(value: ComposeDestination) = root.update { value }
-}
diff --git a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt b/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt
deleted file mode 100644
index 987aa3d0059d..000000000000
--- a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Nextcloud - Android Client
- *
- * SPDX-FileCopyrightText: 2024 Alper Ozturk
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH
- * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
- */
-package com.nextcloud.ui.composeComponents.alertDialog
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.FilledTonalButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.owncloud.android.R
-
-@Composable
-fun SimpleAlertDialog(
- title: String,
- description: String?,
- heightFraction: Float? = null,
- content: @Composable (() -> Unit)? = null,
- onComplete: () -> Unit,
- onDismiss: () -> Unit
-) {
- val modifier = if (heightFraction != null) {
- Modifier
- .fillMaxWidth()
- .fillMaxHeight(heightFraction)
- } else {
- Modifier.fillMaxWidth()
- }
-
- AlertDialog(
- containerColor = MaterialTheme.colorScheme.surface,
- iconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
- titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
- textContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
- onDismissRequest = { onDismiss() },
- title = {
- Text(text = title)
- },
- text = {
- Column(modifier = modifier) {
- description?.let {
- Text(text = description)
- }
-
- content?.let {
- Spacer(modifier = Modifier.height(16.dp))
-
- content()
- }
- }
- },
- confirmButton = {
- FilledTonalButton(onClick = {
- onComplete()
- onDismiss()
- }) {
- Text(
- stringResource(id = R.string.common_ok)
- )
- }
- },
- dismissButton = {
- TextButton(onClick = { onDismiss() }) {
- Text(
- stringResource(id = R.string.common_cancel)
- )
- }
- }
- )
-}
diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/ClientIntegration.kt b/app/src/main/java/com/nextcloud/ui/fileactions/ClientIntegration.kt
index 1857507773b8..ef0b7b247e73 100644
--- a/app/src/main/java/com/nextcloud/ui/fileactions/ClientIntegration.kt
+++ b/app/src/main/java/com/nextcloud/ui/fileactions/ClientIntegration.kt
@@ -35,8 +35,8 @@ import com.nextcloud.client.account.User
import com.nextcloud.common.JSONRequestBody
import com.nextcloud.operations.GetMethod
import com.nextcloud.operations.PostMethod
-import com.nextcloud.ui.composeActivity.ComposeActivity
-import com.nextcloud.ui.composeActivity.ComposeDestination
+import com.nextcloud.ui.compose.ComposeActivity
+import com.nextcloud.ui.compose.navigation.ComposeDestination
import com.nextcloud.utils.GlideHelper
import com.nextcloud.utils.SnackbarUtil
import com.nextcloud.utils.view.ScreenMetrics
diff --git a/app/src/main/java/com/nextcloud/ui/teams/TeamsScreen.kt b/app/src/main/java/com/nextcloud/ui/teams/TeamsScreen.kt
new file mode 100644
index 000000000000..f0d7568e2e06
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/TeamsScreen.kt
@@ -0,0 +1,153 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.nextcloud.android.lib.resources.teams.model.Team
+import com.nextcloud.ui.teams.component.CreateTeamDialog
+import com.nextcloud.ui.teams.component.TeamsErrorMessage
+import com.nextcloud.ui.teams.component.TeamsMessage
+import com.nextcloud.ui.teams.component.TeamsSearchBar
+import com.nextcloud.ui.teams.model.TeamsUiState
+import com.nextcloud.ui.teams.viewmodel.TeamsViewModel
+import com.owncloud.android.R
+
+@Composable
+fun TeamsScreen(viewModel: TeamsViewModel, onTeamClick: (Team) -> Unit) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle()
+ val createTeamName by viewModel.createTeamName.collectAsStateWithLifecycle()
+ val createTeamStatus by viewModel.createTeamStatus.collectAsStateWithLifecycle()
+
+ Scaffold(
+ contentWindowInsets = WindowInsets(0),
+ floatingActionButton = {
+ ExtendedFloatingActionButton(
+ text = { Text(stringResource(R.string.teams_create)) },
+ icon = { Icon(painterResource(R.drawable.ic_plus), contentDescription = null) },
+ onClick = { viewModel.updateCreateTeamStatus("") }
+ )
+ }
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ contentAlignment = Alignment.Center
+ ) {
+ when (val state = uiState) {
+ TeamsUiState.Loading -> CircularProgressIndicator()
+ is TeamsUiState.Error -> TeamsErrorMessage(state.messageId, onRetry = viewModel::retry)
+ is TeamsUiState.Content -> TeamsContent(state, searchQuery, viewModel::updateSearchQuery, onTeamClick)
+ }
+ }
+ }
+
+ createTeamName?.let { name ->
+ CreateTeamDialog(
+ name = name,
+ status = createTeamStatus,
+ onNameChange = viewModel::updateCreateTeamStatus,
+ onDismiss = { viewModel.updateCreateTeamStatus(null) },
+ onCreate = viewModel::createTeam
+ )
+ }
+}
+
+@Composable
+private fun TeamsContent(
+ state: TeamsUiState.Content,
+ searchQuery: String,
+ onSearchQueryChange: (String) -> Unit,
+ onTeamClick: (Team) -> Unit
+) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ TeamsSearchBar(searchQuery, onSearchQueryChange)
+
+ if (state.isEmpty()) {
+ val messageId = if (searchQuery.isBlank()) R.string.teams_empty else R.string.teams_search_no_results
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ TeamsMessage(messageId)
+ }
+ } else {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ teamSection(R.string.teams_my_teams, state.myTeams, onTeamClick)
+ teamSection(R.string.teams_other_teams, state.otherTeams, onTeamClick)
+ }
+ }
+ }
+}
+
+private fun LazyListScope.teamSection(@StringRes titleId: Int, teams: List, onTeamClick: (Team) -> Unit) {
+ if (teams.isEmpty()) {
+ return
+ }
+
+ item(key = titleId) { SectionHeader(titleId) }
+ items(teams, key = { it.id }) { team -> TeamListItem(team) { onTeamClick(team) } }
+}
+
+@Composable
+private fun SectionHeader(@StringRes titleId: Int) {
+ Text(
+ text = stringResource(titleId),
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(
+ start = dimensionResource(R.dimen.standard_padding),
+ end = dimensionResource(R.dimen.standard_padding),
+ top = dimensionResource(R.dimen.standard_padding),
+ bottom = dimensionResource(R.dimen.standard_half_padding)
+ )
+ )
+}
+
+@Composable
+private fun TeamListItem(team: Team, onClick: () -> Unit) {
+ ListItem(
+ modifier = Modifier.clickable(onClick = onClick),
+ headlineContent = {
+ Text(text = team.displayName, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ },
+ supportingContent = {
+ Text(pluralStringResource(R.plurals.teams_member_count, team.population, team.population))
+ },
+ leadingContent = {
+ Icon(
+ painter = painterResource(R.drawable.ic_circles),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ )
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/component/CreateTeamDialog.kt b/app/src/main/java/com/nextcloud/ui/teams/component/CreateTeamDialog.kt
new file mode 100644
index 000000000000..5ed57cf1fdb4
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/component/CreateTeamDialog.kt
@@ -0,0 +1,109 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.component
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.nextcloud.ui.compose.component.alertDialog.BaseAlertDialog
+import com.nextcloud.ui.teams.model.TeamDialogStatus
+import com.nextcloud.ui.teams.viewmodel.TeamsViewModel
+import com.owncloud.android.R
+
+@Composable
+fun CreateTeamDialog(
+ name: String,
+ status: TeamDialogStatus,
+ onNameChange: (String) -> Unit,
+ onDismiss: () -> Unit,
+ onCreate: () -> Unit
+) {
+ BaseAlertDialog(
+ title = stringResource(R.string.teams_create),
+ onDismiss = onDismiss,
+ onComplete = onCreate,
+ completeText = stringResource(R.string.create),
+ isCompleteEnabled = canCreate(name, status)
+ ) {
+ CreateTeamDialogFields(name, status, onNameChange)
+ }
+}
+
+private fun canCreate(name: String, status: TeamDialogStatus): Boolean =
+ TeamsViewModel.isValidTeamName(name) && status != TeamDialogStatus.Submitting
+
+@Composable
+private fun CreateTeamDialogFields(name: String, status: TeamDialogStatus, onNameChange: (String) -> Unit) {
+ val isCreating = status == TeamDialogStatus.Submitting
+
+ OutlinedTextField(
+ value = name,
+ onValueChange = onNameChange,
+ label = { Text(stringResource(R.string.teams_name_hint)) },
+ supportingText = { CreateTeamSupportingText(name, status) },
+ isError = status is TeamDialogStatus.Failed,
+ readOnly = isCreating,
+ singleLine = true
+ )
+ if (isCreating) {
+ LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ }
+}
+
+@Composable
+private fun CreateTeamSupportingText(name: String, status: TeamDialogStatus) {
+ if (status is TeamDialogStatus.Failed) {
+ Text(stringResource(status.messageId))
+ return
+ }
+
+ if (name.isNotBlank() && !TeamsViewModel.isValidTeamName(name)) {
+ Text(
+ pluralStringResource(
+ R.plurals.teams_name_length,
+ TeamsViewModel.MAX_TEAM_NAME_BYTES,
+ TeamsViewModel.MIN_TEAM_NAME_BYTES,
+ TeamsViewModel.MAX_TEAM_NAME_BYTES
+ )
+ )
+ }
+}
+
+@Composable
+private fun CreateTeamDialogPreview(name: String, status: TeamDialogStatus) {
+ MaterialTheme {
+ CreateTeamDialog(name = name, status = status, onNameChange = {}, onDismiss = {}, onCreate = {})
+ }
+}
+
+@Preview
+@Composable
+private fun CreateTeamDialogEditingPreview() {
+ CreateTeamDialogPreview(name = "Editing", status = TeamDialogStatus.Editing)
+}
+
+@Preview
+@Composable
+private fun CreateTeamDialogCreatingPreview() {
+ CreateTeamDialogPreview(name = "Creating", status = TeamDialogStatus.Submitting)
+}
+
+@Preview
+@Composable
+private fun CreateTeamDialogFailedPreview() {
+ CreateTeamDialogPreview(
+ name = "Failed",
+ status = TeamDialogStatus.Failed(R.string.teams_error_create_not_allowed)
+ )
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/component/TeamsMessage.kt b/app/src/main/java/com/nextcloud/ui/teams/component/TeamsMessage.kt
new file mode 100644
index 000000000000..6197cd2f560f
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/component/TeamsMessage.kt
@@ -0,0 +1,41 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.component
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import com.owncloud.android.R
+
+@Composable
+fun TeamsErrorMessage(@StringRes messageId: Int, onRetry: () -> Unit) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.standard_half_padding))
+ ) {
+ TeamsMessage(messageId)
+ TextButton(onClick = onRetry) {
+ Text(stringResource(R.string.retry))
+ }
+ }
+}
+
+@Composable
+fun TeamsMessage(@StringRes messageId: Int) {
+ Text(
+ text = stringResource(messageId),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/component/TeamsSearchBar.kt b/app/src/main/java/com/nextcloud/ui/teams/component/TeamsSearchBar.kt
new file mode 100644
index 000000000000..5d82e97fe29b
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/component/TeamsSearchBar.kt
@@ -0,0 +1,66 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.component
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DockedSearchBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.SearchBarDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.owncloud.android.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TeamsSearchBar(query: String, onQueryChange: (String) -> Unit) {
+ val focusManager = LocalFocusManager.current
+
+ DockedSearchBar(
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = query,
+ onQueryChange = onQueryChange,
+ onSearch = { focusManager.clearFocus() },
+ expanded = false,
+ onExpandedChange = {},
+ placeholder = { Text(stringResource(R.string.teams_search_hint)) },
+ leadingIcon = { Icon(painterResource(R.drawable.ic_search), contentDescription = null) },
+ trailingIcon = { ClearSearchButton(query, onQueryChange) }
+ )
+ },
+ expanded = false,
+ onExpandedChange = {},
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = dimensionResource(R.dimen.standard_padding),
+ vertical = dimensionResource(R.dimen.standard_half_padding)
+ )
+ ) {}
+}
+
+@Composable
+private fun ClearSearchButton(query: String, onQueryChange: (String) -> Unit) {
+ if (query.isEmpty()) {
+ return
+ }
+
+ IconButton(onClick = { onQueryChange("") }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_close),
+ contentDescription = stringResource(R.string.teams_search_clear)
+ )
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/TeamDetailScreen.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/TeamDetailScreen.kt
new file mode 100644
index 000000000000..18a962b04e79
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/TeamDetailScreen.kt
@@ -0,0 +1,129 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.PrimaryTabRow
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.nextcloud.ui.teams.detail.component.AddTeamMemberDialog
+import com.nextcloud.ui.teams.detail.component.TeamDetailListStateContent
+import com.nextcloud.ui.teams.detail.component.TeamMemberListItem
+import com.nextcloud.ui.teams.detail.component.TeamResourceListItem
+import com.nextcloud.ui.teams.detail.model.TeamDetailTab
+import com.nextcloud.ui.teams.detail.model.TeamResourceTarget
+import com.nextcloud.ui.teams.detail.viewmodel.TeamDetailViewModel
+import com.owncloud.android.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TeamDetailScreen(
+ viewModel: TeamDetailViewModel,
+ title: String,
+ onBack: () -> Unit,
+ onOpenSettings: () -> Unit,
+ onOpenResource: (TeamResourceTarget) -> Unit
+) {
+ val selectedTab by viewModel.selectedTab.collectAsStateWithLifecycle()
+ val resources by viewModel.resources.collectAsStateWithLifecycle()
+ val members by viewModel.members.collectAsStateWithLifecycle()
+ val addMemberQuery by viewModel.addMemberQuery.collectAsStateWithLifecycle()
+ val addMemberStatus by viewModel.addMemberStatus.collectAsStateWithLifecycle()
+ val memberSuggestions by viewModel.memberSuggestions.collectAsStateWithLifecycle()
+
+ Scaffold(
+ contentWindowInsets = WindowInsets(0),
+ topBar = {
+ TopAppBar(
+ title = { Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(painterResource(R.drawable.ic_arrow_back), stringResource(R.string.common_back))
+ }
+ },
+ actions = {
+ IconButton(onClick = onOpenSettings) {
+ Icon(painterResource(R.drawable.ic_settings), stringResource(R.string.team_settings))
+ }
+ },
+ windowInsets = WindowInsets(0)
+ )
+ },
+ floatingActionButton = {
+ if (selectedTab == TeamDetailTab.MEMBERS) {
+ ExtendedFloatingActionButton(
+ text = { Text(stringResource(R.string.team_add_member)) },
+ icon = { Icon(painterResource(R.drawable.ic_account_plus), contentDescription = null) },
+ onClick = { viewModel.updateMemberStatus("") }
+ )
+ }
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+ PrimaryTabRow(selectedTabIndex = selectedTab.ordinal) {
+ TeamDetailTab.entries.forEach { tab ->
+ Tab(
+ selected = tab == selectedTab,
+ onClick = { viewModel.selectTab(tab) },
+ text = { Text(stringResource(tab.titleId)) }
+ )
+ }
+ }
+
+ when (selectedTab) {
+ TeamDetailTab.RESOURCES -> TeamDetailListStateContent(
+ state = resources,
+ emptyMessageId = R.string.team_resources_empty,
+ onRetry = viewModel::retryResources,
+ key = { "${it.provider.id}:${it.id}" }
+ ) { resource ->
+ TeamResourceListItem(resource) { onOpenResource(viewModel.targetFor(resource)) }
+ }
+
+ TeamDetailTab.MEMBERS -> TeamDetailListStateContent(
+ state = members,
+ emptyMessageId = R.string.team_members_empty,
+ onRetry = viewModel::retryMembers,
+ key = { it.singleId }
+ ) { member ->
+ TeamMemberListItem(member)
+ }
+ }
+ }
+ }
+
+ addMemberQuery?.let { query ->
+ AddTeamMemberDialog(
+ query = query,
+ status = addMemberStatus,
+ suggestions = memberSuggestions,
+ onQueryChange = viewModel::updateMemberStatus,
+ onSelect = viewModel::addMember,
+ onDismiss = { viewModel.updateMemberStatus(null) }
+ )
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/TeamResourceOpener.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/TeamResourceOpener.kt
new file mode 100644
index 000000000000..c6a5f104a1dd
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/TeamResourceOpener.kt
@@ -0,0 +1,67 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import com.nextcloud.client.account.User
+import com.nextcloud.ui.teams.detail.model.TeamResourceTarget
+import com.owncloud.android.ui.activity.FileDisplayActivity
+
+class TeamResourceOpener(private val activity: Activity, private val user: User) {
+
+ fun open(target: TeamResourceTarget) {
+ when (target) {
+ is TeamResourceTarget.Folder -> openInFiles(FileDisplayActivity.KEY_FILE_PATH, target.remotePath)
+ is TeamResourceTarget.File -> openInFiles(FileDisplayActivity.KEY_FILE_ID, target.fileId)
+ is TeamResourceTarget.TalkConversation -> openTalkConversation(target)
+ is TeamResourceTarget.WebPage -> openWebPage(target)
+ }
+ }
+
+ private fun openInFiles(key: String, value: String) {
+ val intent = Intent(activity, FileDisplayActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ putExtra(FileDisplayActivity.KEY_ACCOUNT, user.accountName)
+ putExtra(key, value)
+ }
+ activity.startActivity(intent)
+ }
+
+ private fun openWebPage(target: TeamResourceTarget.WebPage) {
+ activity.startActivity(TeamWebView.intent(activity, target.title, target.url))
+ }
+
+ private fun openTalkConversation(target: TeamResourceTarget.TalkConversation) {
+ val intent = Intent(Intent.ACTION_VIEW, talkConversationUri(target.token))
+ try {
+ activity.startActivity(intent)
+ } catch (_: ActivityNotFoundException) {
+ openWebPage(target.webPage)
+ }
+ }
+
+ private fun talkConversationUri(token: String): Uri {
+ val server = user.server.uri
+ val userId = Uri.encode(user.accountName.substringBeforeLast(ACCOUNT_SEPARATOR))
+ val basePath = server.rawPath.orEmpty().trimEnd(PATH_SEPARATOR)
+ return Uri.Builder().apply {
+ scheme(TALK_SCHEME)
+ encodedAuthority("$userId$ACCOUNT_SEPARATOR${server.rawAuthority}")
+ encodedPath("$basePath$TALK_CALL_PATH$token")
+ }.build()
+ }
+
+ companion object {
+ private const val TALK_SCHEME = "nextcloudtalk"
+ private const val TALK_CALL_PATH = "/call/"
+ private const val ACCOUNT_SEPARATOR = '@'
+ private const val PATH_SEPARATOR = '/'
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/TeamWebView.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/TeamWebView.kt
new file mode 100644
index 000000000000..e5a1f83234b2
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/TeamWebView.kt
@@ -0,0 +1,86 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.webkit.CookieManager
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import androidx.core.net.toUri
+import androidx.lifecycle.lifecycleScope
+import com.nextcloud.client.utils.IntentUtil
+import com.owncloud.android.ui.NextcloudWebViewClient
+import com.owncloud.android.ui.activity.ExternalSiteWebView
+import kotlinx.coroutines.launch
+
+class TeamWebView : ExternalSiteWebView() {
+
+ override fun postOnCreate() {
+ super.postOnCreate()
+ val pageUrl = intent.getStringExtra(EXTRA_PAGE_URL) ?: return
+
+ webView.settings.userAgentString = null
+ webView.webViewClient = object : NextcloudWebViewClient(supportFragmentManager) {
+ override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest): Boolean {
+ if (request.url.hasSameOrigin(pageUrl.toUri())) {
+ return false
+ }
+
+ IntentUtil.startLinkIntent(this@TeamWebView, request.url)
+ return true
+ }
+ }
+
+ proxyUtil?.applyProxy { loadSignedIn(pageUrl) }
+ }
+
+ private fun loadSignedIn(pageUrl: String) {
+ lifecycleScope.launch {
+ val client = clientRepository.getNextcloudClient()
+ if (client == null || !pageUrl.toUri().hasSameOrigin(client.baseUri)) {
+ webView.loadUrl(pageUrl)
+ return@launch
+ }
+
+ val cookieManager = CookieManager.getInstance()
+ cookieManager.removeAllCookies {
+ cookieManager.setCookies(client.baseUri.toString(), BROWSER_STATE_COOKIES) {
+ webView.loadUrl(pageUrl, mapOf(AUTHORIZATION_HEADER to client.credentials))
+ }
+ }
+ }
+ }
+
+ private fun CookieManager.setCookies(url: String, cookies: List, onDone: () -> Unit) {
+ if (cookies.isEmpty()) {
+ onDone()
+ return
+ }
+ setCookie(url, cookies.first()) { setCookies(url, cookies.drop(1), onDone) }
+ }
+
+ private fun Uri.hasSameOrigin(other: Uri): Boolean =
+ scheme == other.scheme && host == other.host && port == other.port
+
+ companion object {
+ private const val EXTRA_PAGE_URL = "PAGE_URL"
+ private const val AUTHORIZATION_HEADER = "Authorization"
+ private val BROWSER_STATE_COOKIES = listOf(
+ "cookie_test=test; path=/",
+ "nc_sameSiteCookielax=true; path=/; SameSite=Lax",
+ "nc_sameSiteCookiestrict=true; path=/; SameSite=Strict"
+ )
+
+ fun intent(context: Context, title: String, pageUrl: String): Intent =
+ Intent(context, TeamWebView::class.java).apply {
+ putExtra(EXTRA_TITLE, title)
+ putExtra(EXTRA_PAGE_URL, pageUrl)
+ }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/component/AddTeamMemberDialog.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/component/AddTeamMemberDialog.kt
new file mode 100644
index 000000000000..295c0199e89a
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/component/AddTeamMemberDialog.kt
@@ -0,0 +1,78 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import com.nextcloud.ui.compose.component.alertDialog.BaseAlertDialog
+import com.nextcloud.ui.teams.model.TeamDialogStatus
+import com.nextcloud.ui.teams.detail.model.TeamMemberSuggestion
+import com.nextcloud.ui.teams.util.iconId
+import com.owncloud.android.R
+
+@Composable
+fun AddTeamMemberDialog(
+ query: String,
+ status: TeamDialogStatus,
+ suggestions: List,
+ onQueryChange: (String) -> Unit,
+ onSelect: (TeamMemberSuggestion) -> Unit,
+ onDismiss: () -> Unit
+) {
+ val isAdding = status == TeamDialogStatus.Submitting
+ val failure = status as? TeamDialogStatus.Failed
+
+ BaseAlertDialog(title = stringResource(R.string.team_add_member), onDismiss = onDismiss) {
+ OutlinedTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ label = { Text(stringResource(R.string.team_add_member_hint)) },
+ supportingText = failure?.let { { Text(stringResource(it.messageId)) } },
+ isError = failure != null,
+ readOnly = isAdding,
+ singleLine = true
+ )
+ if (isAdding) {
+ LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ }
+ LazyColumn(modifier = Modifier.heightIn(max = dimensionResource(R.dimen.team_member_suggestions_max_height))) {
+ items(suggestions, key = { "${it.member.type}:${it.member.id}" }) { suggestion ->
+ ListItem(
+ headlineContent = {
+ Text(text = suggestion.label, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ },
+ leadingContent = {
+ Icon(
+ painter = painterResource(suggestion.member.type.iconId()),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ },
+ colors = ListItemDefaults.colors(containerColor = Color.Transparent),
+ modifier = Modifier.clickable(enabled = !isAdding) { onSelect(suggestion) }
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamDetailListStateContent.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamDetailListStateContent.kt
new file mode 100644
index 000000000000..e9280b0421fc
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamDetailListStateContent.kt
@@ -0,0 +1,44 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.component
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.nextcloud.ui.teams.component.TeamsErrorMessage
+import com.nextcloud.ui.teams.component.TeamsMessage
+import com.nextcloud.ui.teams.detail.model.TeamDetailListUiState
+
+@Composable
+fun TeamDetailListStateContent(
+ state: TeamDetailListUiState,
+ @StringRes emptyMessageId: Int,
+ onRetry: () -> Unit,
+ key: (T) -> Any,
+ itemContent: @Composable (T) -> Unit
+) {
+ if (state is TeamDetailListUiState.Content && state.items.isNotEmpty()) {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(state.items, key = key) { itemContent(it) }
+ }
+ return
+ }
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ when (state) {
+ TeamDetailListUiState.Loading -> CircularProgressIndicator()
+ is TeamDetailListUiState.Error -> TeamsErrorMessage(state.messageId, onRetry)
+ is TeamDetailListUiState.Content -> TeamsMessage(emptyMessageId)
+ }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamMemberListItem.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamMemberListItem.kt
new file mode 100644
index 000000000000..1b17c4c1f664
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamMemberListItem.kt
@@ -0,0 +1,40 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.component
+
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import com.nextcloud.android.lib.resources.teams.model.TeamMember
+import com.nextcloud.ui.teams.util.iconId
+import com.nextcloud.ui.teams.util.titleId
+
+@Composable
+fun TeamMemberListItem(member: TeamMember) {
+ ListItem(
+ headlineContent = {
+ Text(
+ text = member.displayName.ifEmpty { member.userId.orEmpty() },
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ supportingContent = { Text(stringResource(member.role.titleId())) },
+ leadingContent = {
+ Icon(
+ painter = painterResource(member.type.iconId()),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ )
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamResourceListItem.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamResourceListItem.kt
new file mode 100644
index 000000000000..a66c6a121c0a
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/component/TeamResourceListItem.kt
@@ -0,0 +1,39 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import com.nextcloud.android.lib.resources.teams.get.resources.model.TeamResource
+import com.nextcloud.ui.teams.util.iconId
+import com.owncloud.android.R
+
+@Composable
+fun TeamResourceListItem(resource: TeamResource, onClick: () -> Unit) {
+ ListItem(
+ headlineContent = { Text(text = resource.label, maxLines = 1, overflow = TextOverflow.Ellipsis) },
+ supportingContent = { Text(resource.provider.name) },
+ leadingContent = {
+ Icon(
+ painter = painterResource(resource.provider.iconId()),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(dimensionResource(R.dimen.iconized_single_line_item_icon_size))
+ )
+ },
+ modifier = Modifier.clickable(onClick = onClick)
+ )
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailErrorMessages.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailErrorMessages.kt
new file mode 100644
index 000000000000..3e9a74c2acbb
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailErrorMessages.kt
@@ -0,0 +1,26 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.model
+
+import androidx.annotation.StringRes
+import com.nextcloud.utils.extensions.connectionErrorMessageId
+import com.owncloud.android.R
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import java.net.HttpURLConnection
+
+@StringRes
+fun RemoteOperationResult<*>.loadTeamDetailErrorMessageId(): Int = connectionErrorMessageId() ?: when (httpCode) {
+ HttpURLConnection.HTTP_NOT_FOUND -> R.string.team_error_not_found
+ else -> R.string.team_load_failed
+}
+
+@StringRes
+fun RemoteOperationResult<*>.addTeamMemberErrorMessageId(): Int = connectionErrorMessageId() ?: when (httpCode) {
+ HttpURLConnection.HTTP_FORBIDDEN -> R.string.team_error_add_member_not_allowed
+ HttpURLConnection.HTTP_BAD_REQUEST -> R.string.team_error_add_member_invalid
+ else -> R.string.team_add_member_failed
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailListUiState.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailListUiState.kt
new file mode 100644
index 000000000000..637941eb2436
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailListUiState.kt
@@ -0,0 +1,17 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.model
+
+import androidx.annotation.StringRes
+
+sealed interface TeamDetailListUiState {
+ data object Loading : TeamDetailListUiState
+
+ data class Error(@StringRes val messageId: Int) : TeamDetailListUiState
+
+ data class Content(val items: List) : TeamDetailListUiState
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailTab.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailTab.kt
new file mode 100644
index 000000000000..9c917bab3aec
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamDetailTab.kt
@@ -0,0 +1,15 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.model
+
+import androidx.annotation.StringRes
+import com.owncloud.android.R
+
+enum class TeamDetailTab(@StringRes val titleId: Int) {
+ RESOURCES(R.string.team_tab_resources),
+ MEMBERS(R.string.team_tab_members)
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamMemberSuggestion.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamMemberSuggestion.kt
new file mode 100644
index 000000000000..4a6af7ddbd3c
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamMemberSuggestion.kt
@@ -0,0 +1,11 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.model
+
+import com.nextcloud.android.lib.resources.teams.post.members.model.NewTeamMember
+
+data class TeamMemberSuggestion(val label: String, val member: NewTeamMember)
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamResourceProviderId.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamResourceProviderId.kt
new file mode 100644
index 000000000000..98b3f72850c7
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamResourceProviderId.kt
@@ -0,0 +1,19 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.model
+
+enum class TeamResourceProviderId(val id: String) {
+ FILES("files"),
+ TEAM_FOLDER("groupfolders"),
+ TALK("talk"),
+ DECK("deck"),
+ COLLECTIVES("collectives");
+
+ companion object {
+ fun from(id: String): TeamResourceProviderId? = entries.firstOrNull { it.id == id }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamResourceTarget.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamResourceTarget.kt
new file mode 100644
index 000000000000..daaa9dd43766
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/model/TeamResourceTarget.kt
@@ -0,0 +1,17 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.model
+
+sealed interface TeamResourceTarget {
+ data class Folder(val remotePath: String) : TeamResourceTarget
+
+ data class File(val fileId: String) : TeamResourceTarget
+
+ data class TalkConversation(val token: String, val webPage: WebPage) : TeamResourceTarget
+
+ data class WebPage(val url: String, val title: String) : TeamResourceTarget
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/repository/TeamDetailRemoteRepository.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/repository/TeamDetailRemoteRepository.kt
new file mode 100644
index 000000000000..9ead409ea5c8
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/repository/TeamDetailRemoteRepository.kt
@@ -0,0 +1,73 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.repository
+
+import com.nextcloud.android.lib.resources.collectives.GetCollectivesUserFolderRemoteOperation
+import com.nextcloud.android.lib.resources.collectives.model.CollectivesUserFolder
+import com.nextcloud.android.lib.resources.teams.get.folder.GetTeamFolderRemoteOperation
+import com.nextcloud.android.lib.resources.teams.get.folder.model.TeamFolder
+import com.nextcloud.android.lib.resources.teams.get.members.GetTeamMembersRemoteOperation
+import com.nextcloud.android.lib.resources.teams.get.resources.GetTeamResourcesRemoteOperation
+import com.nextcloud.android.lib.resources.teams.get.resources.model.TeamResource
+import com.nextcloud.android.lib.resources.teams.model.TeamMember
+import com.nextcloud.android.lib.resources.teams.model.TeamMemberType
+import com.nextcloud.android.lib.resources.teams.post.members.AddTeamMembersRemoteOperation
+import com.nextcloud.android.lib.resources.teams.post.members.model.NewTeamMember
+import com.nextcloud.repository.ClientRepository
+import com.nextcloud.ui.teams.detail.model.TeamMemberSuggestion
+import com.nextcloud.utils.extensions.execute
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation
+import com.owncloud.android.lib.resources.shares.ShareType
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.json.JSONObject
+import javax.inject.Inject
+
+class TeamDetailRemoteRepository @Inject constructor(private val clientRepository: ClientRepository) :
+ TeamDetailRepository {
+
+ override suspend fun getResources(teamId: String): RemoteOperationResult> =
+ clientRepository.execute(GetTeamResourcesRemoteOperation(teamId))
+
+ override suspend fun getFolder(teamId: String): RemoteOperationResult =
+ clientRepository.execute(GetTeamFolderRemoteOperation(teamId))
+
+ override suspend fun getCollectivesUserFolder(): RemoteOperationResult =
+ clientRepository.execute(GetCollectivesUserFolderRemoteOperation())
+
+ override suspend fun getMembers(teamId: String): RemoteOperationResult> =
+ clientRepository.execute(GetTeamMembersRemoteOperation(teamId))
+
+ override suspend fun addMember(teamId: String, member: NewTeamMember): RemoteOperationResult =
+ clientRepository.execute(AddTeamMembersRemoteOperation(teamId, listOf(member)))
+
+ override suspend fun searchMemberSuggestions(query: String): List {
+ val client = clientRepository.getOwncloudClient() ?: return emptyList()
+ return withContext(Dispatchers.IO) {
+ val result = GetShareesRemoteOperation(query, FIRST_PAGE, SUGGESTIONS_PER_PAGE).execute(client)
+ if (!result.isSuccess) {
+ return@withContext emptyList()
+ }
+ result.resultData.orEmpty().mapNotNull { it.toTeamMemberSuggestion() }
+ }
+ }
+
+ private fun JSONObject.toTeamMemberSuggestion(): TeamMemberSuggestion? {
+ val value = optJSONObject(GetShareesRemoteOperation.NODE_VALUE) ?: return null
+ val shareType = ShareType.fromValue(value.optInt(GetShareesRemoteOperation.PROPERTY_SHARE_TYPE))
+ val type = TeamMemberType.fromShareType(shareType) ?: return null
+ val id = value.optString(GetShareesRemoteOperation.PROPERTY_SHARE_WITH).ifEmpty { return null }
+ val label = optString(GetShareesRemoteOperation.PROPERTY_LABEL).ifEmpty { id }
+ return TeamMemberSuggestion(label, NewTeamMember(id, type))
+ }
+
+ companion object {
+ private const val FIRST_PAGE = 1
+ private const val SUGGESTIONS_PER_PAGE = 20
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/repository/TeamDetailRepository.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/repository/TeamDetailRepository.kt
new file mode 100644
index 000000000000..96c6dd4e5e82
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/repository/TeamDetailRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.repository
+
+import com.nextcloud.android.lib.resources.collectives.model.CollectivesUserFolder
+import com.nextcloud.android.lib.resources.teams.get.folder.model.TeamFolder
+import com.nextcloud.android.lib.resources.teams.get.resources.model.TeamResource
+import com.nextcloud.android.lib.resources.teams.model.TeamMember
+import com.nextcloud.android.lib.resources.teams.post.members.model.NewTeamMember
+import com.nextcloud.ui.teams.detail.model.TeamMemberSuggestion
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+
+interface TeamDetailRepository {
+ suspend fun getResources(teamId: String): RemoteOperationResult>
+
+ suspend fun getFolder(teamId: String): RemoteOperationResult
+
+ suspend fun getCollectivesUserFolder(): RemoteOperationResult
+
+ suspend fun getMembers(teamId: String): RemoteOperationResult>
+
+ suspend fun addMember(teamId: String, member: NewTeamMember): RemoteOperationResult
+
+ suspend fun searchMemberSuggestions(query: String): List
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/detail/viewmodel/TeamDetailViewModel.kt b/app/src/main/java/com/nextcloud/ui/teams/detail/viewmodel/TeamDetailViewModel.kt
new file mode 100644
index 000000000000..bc240676db89
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/detail/viewmodel/TeamDetailViewModel.kt
@@ -0,0 +1,190 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.detail.viewmodel
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.nextcloud.android.lib.resources.teams.get.resources.model.TeamResource
+import com.nextcloud.android.lib.resources.teams.model.TeamMember
+import com.nextcloud.client.di.SavedStateViewModelCreator
+import com.nextcloud.ui.teams.model.TeamDialogStatus
+import com.nextcloud.ui.teams.detail.model.TeamDetailListUiState
+import com.nextcloud.ui.teams.detail.model.TeamDetailTab
+import com.nextcloud.ui.teams.detail.model.TeamMemberSuggestion
+import com.nextcloud.ui.teams.detail.model.TeamResourceProviderId
+import com.nextcloud.ui.teams.detail.model.TeamResourceTarget
+import com.nextcloud.ui.teams.detail.model.addTeamMemberErrorMessageId
+import com.nextcloud.ui.teams.detail.model.loadTeamDetailErrorMessageId
+import com.nextcloud.ui.teams.detail.repository.TeamDetailRepository
+import com.nextcloud.ui.teams.util.providerId
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+
+class TeamDetailViewModel @AssistedInject constructor(
+ private val repository: TeamDetailRepository,
+ @Assisted private val savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val teamId: String = checkNotNull(savedStateHandle[KEY_TEAM_ID])
+
+ private var folderPath: String? = null
+
+ private var collectivesFolderPath: String? = null
+
+ val selectedTab: StateFlow = savedStateHandle.getStateFlow(KEY_SELECTED_TAB, TeamDetailTab.RESOURCES)
+
+ private val _resources = MutableStateFlow>(TeamDetailListUiState.Loading)
+ val resources: StateFlow> = _resources.asStateFlow()
+
+ private val _members = MutableStateFlow>(TeamDetailListUiState.Loading)
+ val members: StateFlow> = _members.asStateFlow()
+
+ val addMemberQuery: StateFlow = savedStateHandle.getStateFlow(KEY_ADD_MEMBER_QUERY, null)
+
+ private val _addMemberStatus = MutableStateFlow(TeamDialogStatus.Editing)
+ val addMemberStatus: StateFlow = _addMemberStatus.asStateFlow()
+
+ @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
+ private val searchResults = addMemberQuery
+ .debounce(SEARCH_DEBOUNCE)
+ .mapLatest { query ->
+ if (query.isNullOrBlank()) emptyList() else repository.searchMemberSuggestions(query.trim())
+ }
+
+ val memberSuggestions: StateFlow> =
+ combine(searchResults, _members) { suggestions, members ->
+ val currentMembers = (members as? TeamDetailListUiState.Content)?.items.orEmpty()
+ val memberIds = currentMembers.mapNotNull { it.userId }.toSet()
+ suggestions.filterNot { it.member.id in memberIds }
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT), emptyList())
+
+ init {
+ viewModelScope.launch { loadResources() }
+ viewModelScope.launch { loadMembers() }
+ }
+
+ fun selectTab(tab: TeamDetailTab) {
+ savedStateHandle[KEY_SELECTED_TAB] = tab
+ }
+
+ fun retryResources() {
+ _resources.value = TeamDetailListUiState.Loading
+ viewModelScope.launch { loadResources() }
+ }
+
+ fun retryMembers() {
+ _members.value = TeamDetailListUiState.Loading
+ viewModelScope.launch { loadMembers() }
+ }
+
+ fun targetFor(resource: TeamResource): TeamResourceTarget {
+ val webPage = TeamResourceTarget.WebPage(resource.url, resource.label)
+ return when (resource.provider.providerId) {
+ TeamResourceProviderId.FILES -> TeamResourceTarget.File(resource.id)
+ TeamResourceProviderId.TEAM_FOLDER -> folderPath?.let { TeamResourceTarget.Folder(it) } ?: webPage
+ TeamResourceProviderId.TALK -> TeamResourceTarget.TalkConversation(resource.id, webPage)
+ TeamResourceProviderId.COLLECTIVES -> collectiveFolder(resource.label) ?: webPage
+ TeamResourceProviderId.DECK, null -> webPage
+ }
+ }
+
+ private fun collectiveFolder(collectiveName: String): TeamResourceTarget.Folder? =
+ collectivesFolderPath?.let { TeamResourceTarget.Folder("$it$collectiveName/") }
+
+ fun updateMemberStatus(query: String?) {
+ _addMemberStatus.value = TeamDialogStatus.Editing
+ savedStateHandle[KEY_ADD_MEMBER_QUERY] = query
+ }
+
+ fun addMember(suggestion: TeamMemberSuggestion) {
+ _addMemberStatus.value = TeamDialogStatus.Submitting
+ viewModelScope.launch {
+ val result = repository.addMember(teamId, suggestion.member)
+ if (!result.isSuccess) {
+ val failed = TeamDialogStatus.Failed(result.addTeamMemberErrorMessageId())
+ _addMemberStatus.compareAndSet(TeamDialogStatus.Submitting, failed)
+ return@launch
+ }
+
+ loadMembers()
+ if (_addMemberStatus.compareAndSet(TeamDialogStatus.Submitting, TeamDialogStatus.Editing)) {
+ updateMemberStatus(null)
+ }
+ }
+ }
+
+ private suspend fun loadResources() {
+ val result = repository.getResources(teamId)
+ if (!result.isSuccess) {
+ _resources.value = TeamDetailListUiState.Error(result.loadTeamDetailErrorMessageId())
+ return
+ }
+
+ val resources = result.resultData.orEmpty()
+ if (resources.any { it.provider.providerId == TeamResourceProviderId.TEAM_FOLDER }) {
+ folderPath = loadFolderPath()
+ }
+ if (resources.any { it.provider.providerId == TeamResourceProviderId.COLLECTIVES }) {
+ collectivesFolderPath = loadCollectivesFolderPath()
+ }
+ _resources.value = TeamDetailListUiState.Content(resources)
+ }
+
+ private suspend fun loadCollectivesFolderPath(): String? {
+ val result = repository.getCollectivesUserFolder()
+ if (!result.isSuccess) {
+ return null
+ }
+ return result.resultData?.path?.let { "/${it.trim('/')}/" }
+ }
+
+ private suspend fun loadFolderPath(): String? {
+ val result = repository.getFolder(teamId)
+ if (!result.isSuccess) {
+ return null
+ }
+ return result.resultData?.mountPoint?.let { "/$it/" }
+ }
+
+ private suspend fun loadMembers() {
+ val result = repository.getMembers(teamId)
+ _members.value = if (result.isSuccess) {
+ TeamDetailListUiState.Content(result.resultData.orEmpty())
+ } else {
+ TeamDetailListUiState.Error(result.loadTeamDetailErrorMessageId())
+ }
+ }
+
+ @AssistedFactory
+ interface Factory : SavedStateViewModelCreator {
+ override fun create(savedStateHandle: SavedStateHandle): TeamDetailViewModel
+ }
+
+ companion object {
+ const val KEY_TEAM_ID = "team_id"
+ private const val KEY_SELECTED_TAB = "selected_tab"
+ private const val KEY_ADD_MEMBER_QUERY = "add_member_query"
+ private val SEARCH_DEBOUNCE = 300.milliseconds
+ private val STOP_TIMEOUT = 5.seconds
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/model/TeamDialogStatus.kt b/app/src/main/java/com/nextcloud/ui/teams/model/TeamDialogStatus.kt
new file mode 100644
index 000000000000..ea6198c734a6
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/model/TeamDialogStatus.kt
@@ -0,0 +1,17 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.model
+
+import androidx.annotation.StringRes
+
+sealed interface TeamDialogStatus {
+ data object Editing : TeamDialogStatus
+
+ data object Submitting : TeamDialogStatus
+
+ data class Failed(@StringRes val messageId: Int) : TeamDialogStatus
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/model/TeamsErrorMessages.kt b/app/src/main/java/com/nextcloud/ui/teams/model/TeamsErrorMessages.kt
new file mode 100644
index 000000000000..644b651f7680
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/model/TeamsErrorMessages.kt
@@ -0,0 +1,30 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.model
+
+import androidx.annotation.StringRes
+import com.nextcloud.utils.extensions.connectionErrorMessageId
+import com.owncloud.android.R
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import java.net.HttpURLConnection
+
+private const val HTTP_TOO_MANY_REQUESTS = 429
+
+@StringRes
+fun RemoteOperationResult<*>.loadTeamsErrorMessageId(): Int = connectionErrorMessageId() ?: when (httpCode) {
+ HttpURLConnection.HTTP_NOT_FOUND -> R.string.teams_error_not_available
+ else -> R.string.teams_load_failed
+}
+
+@StringRes
+fun RemoteOperationResult<*>.createTeamErrorMessageId(): Int = connectionErrorMessageId() ?: when (httpCode) {
+ HttpURLConnection.HTTP_BAD_REQUEST -> R.string.teams_error_invalid_name
+ HttpURLConnection.HTTP_FORBIDDEN -> R.string.teams_error_create_not_allowed
+ HttpURLConnection.HTTP_NOT_FOUND -> R.string.teams_error_not_available
+ HTTP_TOO_MANY_REQUESTS -> R.string.teams_error_too_many_requests
+ else -> R.string.teams_create_failed
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/model/TeamsUiState.kt b/app/src/main/java/com/nextcloud/ui/teams/model/TeamsUiState.kt
new file mode 100644
index 000000000000..ea62c939f173
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/model/TeamsUiState.kt
@@ -0,0 +1,30 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.model
+
+import androidx.annotation.StringRes
+import com.nextcloud.android.lib.resources.teams.model.Team
+
+sealed interface TeamsUiState {
+ data object Loading : TeamsUiState
+
+ data class Error(@StringRes val messageId: Int) : TeamsUiState
+
+ data class Content(val myTeams: List, val otherTeams: List) : TeamsUiState {
+ fun isEmpty(): Boolean = myTeams.isEmpty() && otherTeams.isEmpty()
+
+ fun matching(query: String): Content {
+ val trimmedQuery = query.trim()
+ if (trimmedQuery.isEmpty()) {
+ return this
+ }
+
+ val matches: (Team) -> Boolean = { it.displayName.contains(trimmedQuery, ignoreCase = true) }
+ return Content(myTeams.filter(matches), otherTeams.filter(matches))
+ }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/repository/TeamsRemoteRepository.kt b/app/src/main/java/com/nextcloud/ui/teams/repository/TeamsRemoteRepository.kt
new file mode 100644
index 000000000000..365d10ce84c3
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/repository/TeamsRemoteRepository.kt
@@ -0,0 +1,24 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.repository
+
+import com.nextcloud.android.lib.resources.teams.get.team.GetTeamsRemoteOperation
+import com.nextcloud.android.lib.resources.teams.model.Team
+import com.nextcloud.android.lib.resources.teams.post.team.CreateTeamRemoteOperation
+import com.nextcloud.repository.ClientRepository
+import com.nextcloud.utils.extensions.execute
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import javax.inject.Inject
+
+class TeamsRemoteRepository @Inject constructor(private val clientRepository: ClientRepository) : TeamsRepository {
+
+ override suspend fun getTeams(): RemoteOperationResult> =
+ clientRepository.execute(GetTeamsRemoteOperation())
+
+ override suspend fun createTeam(name: String): RemoteOperationResult =
+ clientRepository.execute(CreateTeamRemoteOperation(name))
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/repository/TeamsRepository.kt b/app/src/main/java/com/nextcloud/ui/teams/repository/TeamsRepository.kt
new file mode 100644
index 000000000000..6f21d5a4ce11
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/repository/TeamsRepository.kt
@@ -0,0 +1,16 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.repository
+
+import com.nextcloud.android.lib.resources.teams.model.Team
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+
+interface TeamsRepository {
+ suspend fun getTeams(): RemoteOperationResult>
+
+ suspend fun createTeam(name: String): RemoteOperationResult
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/util/TeamMemberTypeExtensions.kt b/app/src/main/java/com/nextcloud/ui/teams/util/TeamMemberTypeExtensions.kt
new file mode 100644
index 000000000000..9b07eafefefb
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/util/TeamMemberTypeExtensions.kt
@@ -0,0 +1,19 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.util
+
+import androidx.annotation.DrawableRes
+import com.nextcloud.android.lib.resources.teams.model.TeamMemberType
+import com.owncloud.android.R
+
+@DrawableRes
+fun TeamMemberType?.iconId(): Int = when (this) {
+ TeamMemberType.GROUP -> R.drawable.ic_group
+ TeamMemberType.MAIL -> R.drawable.ic_email
+ TeamMemberType.CIRCLE -> R.drawable.ic_circles
+ TeamMemberType.USER, TeamMemberType.CONTACT, null -> R.drawable.ic_user
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/util/TeamResourceProviderExtensions.kt b/app/src/main/java/com/nextcloud/ui/teams/util/TeamResourceProviderExtensions.kt
new file mode 100644
index 000000000000..ce3b105a3439
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/util/TeamResourceProviderExtensions.kt
@@ -0,0 +1,25 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.util
+
+import androidx.annotation.DrawableRes
+import com.nextcloud.android.lib.resources.teams.get.resources.model.TeamResourceProvider
+import com.nextcloud.ui.teams.detail.model.TeamResourceProviderId
+import com.owncloud.android.R
+
+val TeamResourceProvider.providerId: TeamResourceProviderId?
+ get() = TeamResourceProviderId.from(id)
+
+@DrawableRes
+fun TeamResourceProvider.iconId(): Int = when (providerId) {
+ TeamResourceProviderId.FILES -> R.drawable.file
+ TeamResourceProviderId.TEAM_FOLDER -> R.drawable.folder
+ TeamResourceProviderId.TALK -> R.drawable.ic_talk
+ TeamResourceProviderId.DECK -> R.drawable.ic_deck
+ TeamResourceProviderId.COLLECTIVES -> R.drawable.ic_notes
+ null -> R.drawable.ic_link
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/util/TeamRoleExtensions.kt b/app/src/main/java/com/nextcloud/ui/teams/util/TeamRoleExtensions.kt
new file mode 100644
index 000000000000..9bd6fb0a1cd7
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/util/TeamRoleExtensions.kt
@@ -0,0 +1,19 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.util
+
+import androidx.annotation.StringRes
+import com.nextcloud.android.lib.resources.teams.model.TeamRole
+import com.owncloud.android.R
+
+@StringRes
+fun TeamRole.titleId(): Int = when (this) {
+ TeamRole.OWNER -> R.string.team_role_owner
+ TeamRole.ADMIN -> R.string.team_role_admin
+ TeamRole.MODERATOR -> R.string.team_role_moderator
+ TeamRole.MEMBER -> R.string.team_role_member
+}
diff --git a/app/src/main/java/com/nextcloud/ui/teams/viewmodel/TeamsViewModel.kt b/app/src/main/java/com/nextcloud/ui/teams/viewmodel/TeamsViewModel.kt
new file mode 100644
index 000000000000..f2308b18eebc
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/teams/viewmodel/TeamsViewModel.kt
@@ -0,0 +1,115 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.ui.teams.viewmodel
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.nextcloud.client.di.SavedStateViewModelCreator
+import com.nextcloud.ui.teams.model.TeamDialogStatus
+import com.nextcloud.ui.teams.model.TeamsUiState
+import com.nextcloud.ui.teams.model.createTeamErrorMessageId
+import com.nextcloud.ui.teams.model.loadTeamsErrorMessageId
+import com.nextcloud.ui.teams.repository.TeamsRepository
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.WhileSubscribed
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.seconds
+
+class TeamsViewModel @AssistedInject constructor(
+ private val repository: TeamsRepository,
+ @Assisted private val savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val teamsState = MutableStateFlow(TeamsUiState.Loading)
+
+ val searchQuery: StateFlow = savedStateHandle.getStateFlow(KEY_SEARCH_QUERY, "")
+
+ val uiState: StateFlow = combine(teamsState, searchQuery) { state, query ->
+ if (state is TeamsUiState.Content) state.matching(query) else state
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT), TeamsUiState.Loading)
+
+ val createTeamName: StateFlow = savedStateHandle.getStateFlow(KEY_CREATE_TEAM_NAME, null)
+
+ private val _createTeamStatus = MutableStateFlow(TeamDialogStatus.Editing)
+ val createTeamStatus: StateFlow = _createTeamStatus.asStateFlow()
+
+ init {
+ viewModelScope.launch { loadTeams() }
+ }
+
+ fun retry() {
+ teamsState.value = TeamsUiState.Loading
+ viewModelScope.launch { loadTeams() }
+ }
+
+ fun updateSearchQuery(query: String) {
+ savedStateHandle[KEY_SEARCH_QUERY] = query
+ }
+
+ fun updateCreateTeamStatus(name: String?) {
+ _createTeamStatus.value = TeamDialogStatus.Editing
+ savedStateHandle[KEY_CREATE_TEAM_NAME] = name
+ }
+
+ fun createTeam() {
+ val name = createTeamName.value?.trim()
+ if (name == null || !isValidTeamName(name)) {
+ return
+ }
+
+ _createTeamStatus.value = TeamDialogStatus.Submitting
+ viewModelScope.launch {
+ val result = repository.createTeam(name)
+ if (!result.isSuccess) {
+ val failed = TeamDialogStatus.Failed(result.createTeamErrorMessageId())
+ _createTeamStatus.compareAndSet(TeamDialogStatus.Submitting, failed)
+ return@launch
+ }
+
+ loadTeams()
+ if (_createTeamStatus.compareAndSet(TeamDialogStatus.Submitting, TeamDialogStatus.Editing)) {
+ updateCreateTeamStatus(null)
+ }
+ }
+ }
+
+ private suspend fun loadTeams() {
+ val result = repository.getTeams()
+ if (!result.isSuccess) {
+ teamsState.value = TeamsUiState.Error(result.loadTeamsErrorMessageId())
+ return
+ }
+
+ val (myTeams, otherTeams) = result.resultData.orEmpty().partition { it.isMember }
+ teamsState.value = TeamsUiState.Content(myTeams, otherTeams)
+ }
+
+ @AssistedFactory
+ interface Factory : SavedStateViewModelCreator {
+ override fun create(savedStateHandle: SavedStateHandle): TeamsViewModel
+ }
+
+ companion object {
+ private const val KEY_CREATE_TEAM_NAME = "create_team_name"
+ private const val KEY_SEARCH_QUERY = "search_query"
+ private val STOP_TIMEOUT = 5.seconds
+ const val MIN_TEAM_NAME_BYTES = 3
+ const val MAX_TEAM_NAME_BYTES = 127
+
+ fun isValidTeamName(name: String): Boolean =
+ name.trim().toByteArray().size in MIN_TEAM_NAME_BYTES..MAX_TEAM_NAME_BYTES
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ClientRepositoryExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ClientRepositoryExtensions.kt
new file mode 100644
index 000000000000..92a468bb1246
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/utils/extensions/ClientRepositoryExtensions.kt
@@ -0,0 +1,19 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.nextcloud.utils.extensions
+
+import com.nextcloud.repository.ClientRepository
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+suspend fun ClientRepository.execute(operation: RemoteOperation): RemoteOperationResult {
+ val client = getNextcloudClient() ?: return RemoteOperationResult(ResultCode.ACCOUNT_NOT_FOUND)
+ return withContext(Dispatchers.IO) { operation.execute(client) }
+}
diff --git a/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt
index cb5f3781a6ff..c3be081eb7fe 100644
--- a/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt
+++ b/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt
@@ -7,6 +7,7 @@
package com.nextcloud.utils.extensions
+import androidx.annotation.StringRes
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.lib.common.operations.RemoteOperation
@@ -20,6 +21,15 @@ fun Pair?, RemoteOperation<*>?>?.getErrorMessage(): Str
return ErrorMessageAdapter.getErrorCauseMessage(result, operation, MainApp.getAppContext().resources)
}
+@StringRes
+fun RemoteOperationResult<*>.connectionErrorMessageId(): Int? = when (code) {
+ ResultCode.NO_NETWORK_CONNECTION -> R.string.auth_no_net_conn_title
+ ResultCode.HOST_NOT_AVAILABLE, ResultCode.WRONG_CONNECTION -> R.string.network_host_not_available
+ ResultCode.TIMEOUT -> R.string.auth_timeout_title
+ ResultCode.MAINTENANCE_MODE -> R.string.maintenance_mode
+ else -> null
+}
+
fun ResultCode.isFileSpecificError(): Boolean {
val generalErrorCodes = listOf(
ResultCode.INSTANCE_NOT_CONFIGURED,
diff --git a/app/src/main/java/com/owncloud/android/MainApp.kt b/app/src/main/java/com/owncloud/android/MainApp.kt
index f6bfa309fa53..db4d3fb4e7af 100644
--- a/app/src/main/java/com/owncloud/android/MainApp.kt
+++ b/app/src/main/java/com/owncloud/android/MainApp.kt
@@ -40,7 +40,7 @@ import com.nextcloud.client.onboarding.OnboardingService
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.client.preferences.AppPreferencesImpl
import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks
-import com.nextcloud.ui.composeActivity.ComposeProcessTextAlias
+import com.nextcloud.ui.compose.ComposeProcessTextAlias
import com.nextcloud.utils.extensions.registerBroadcastReceiver
import com.nextcloud.utils.extensions.showToast
import com.nextcloud.utils.mdm.MDMConfig
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
index d3ae4fa89eee..feb1d6ef665b 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
+++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
@@ -67,7 +67,7 @@ import com.nextcloud.client.player.model.file.toPlaybackCollection
import com.nextcloud.client.player.ui.audio.AudioPlayerLauncher
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.client.utils.IntentUtil
-import com.nextcloud.ui.composeActivity.ComposeProcessTextAlias
+import com.nextcloud.ui.compose.ComposeProcessTextAlias
import com.nextcloud.utils.SnackbarUtil
import com.nextcloud.utils.extensions.getParcelableArgument
import com.nextcloud.utils.extensions.getSerializableArgument
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/EcosystemAppsBanner.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/EcosystemAppsBanner.kt
index 2e36c0ea8e99..ebfceb896810 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/EcosystemAppsBanner.kt
+++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/header/EcosystemAppsBanner.kt
@@ -43,6 +43,7 @@ class EcosystemAppsBanner(
val talkView = banner.findViewById(R.id.drawer_ecosystem_talk)
val moreView = banner.findViewById(R.id.drawer_ecosystem_more)
val assistantView = banner.findViewById(R.id.drawer_ecosystem_assistant)
+ val teamsView = banner.findViewById(R.id.drawer_ecosystem_teams)
activity.user.getOrNull()?.accountName?.let { accountName ->
notesView.setOnClickListener { activity.ecosystemManager.openApp(EcosystemApp.NOTES, accountName) }
@@ -51,9 +52,10 @@ class EcosystemAppsBanner(
moreView.setOnClickListener { LinkHelper.openAppStore(APP_STORE_QUERY, true, activity) }
assistantView.setOnClickListener { navigator.startAssistantScreen() }
assistantView.isVisible = activity.isAssistantAvailable
+ teamsView.setOnClickListener { navigator.startTeamsScreen() }
val color = iconColor
- listOf(notesView, talkView, moreView, assistantView).forEach { tintAppView(it, color) }
+ listOf(notesView, talkView, moreView, assistantView, teamsView).forEach { tintAppView(it, color) }
banner.visibility = View.VISIBLE
}
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuItemClickHandler.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuItemClickHandler.kt
index c5c6bf2d32ea..a278d824a113 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuItemClickHandler.kt
+++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerMenuItemClickHandler.kt
@@ -7,7 +7,7 @@
package com.owncloud.android.ui.activity.drawer.navigation
import android.view.MenuItem
-import com.nextcloud.ui.composeActivity.ComposeActivity
+import com.nextcloud.ui.compose.ComposeActivity
import com.nextcloud.utils.extensions.navigateToAllFiles
import com.owncloud.android.R
import com.owncloud.android.lib.common.utils.Log_OC
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerNavigator.kt b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerNavigator.kt
index 3e4eda7df728..26d8e3f18e48 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerNavigator.kt
+++ b/app/src/main/java/com/owncloud/android/ui/activity/drawer/navigation/DrawerNavigator.kt
@@ -9,8 +9,8 @@ package com.owncloud.android.ui.activity.drawer.navigation
import android.content.Intent
import android.view.MenuItem
import androidx.fragment.app.commit
-import com.nextcloud.ui.composeActivity.ComposeActivity
-import com.nextcloud.ui.composeActivity.ComposeDestination
+import com.nextcloud.ui.compose.ComposeActivity
+import com.nextcloud.ui.compose.navigation.ComposeDestination
import com.nextcloud.utils.extensions.navigateToAllFiles
import com.owncloud.android.MainApp
import com.owncloud.android.R
@@ -142,6 +142,12 @@ class DrawerNavigator(private val activity: DrawerActivity) {
activity.startActivity(intent)
}
+ fun startTeamsScreen() {
+ val intent = Intent(activity.applicationContext, ComposeActivity::class.java)
+ .putExtra(ComposeActivity.DESTINATION, ComposeDestination.getTeamsScreen(activity))
+ activity.startActivity(intent)
+ }
+
fun resetOnlyPersonalAndOnDevice() {
MainApp.showOnlyFilesOnDevice(false)
MainApp.showOnlyPersonalFiles(false)
diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml
index 88f7ade88c4a..2d3c1b5af00a 100644
--- a/app/src/main/res/layout/drawer_header.xml
+++ b/app/src/main/res/layout/drawer_header.xml
@@ -141,7 +141,7 @@
android:focusable="true"
android:gravity="center_horizontal"
android:orientation="vertical"
- app:layout_constraintEnd_toStartOf="@+id/drawer_ecosystem_more"
+ app:layout_constraintEnd_toStartOf="@+id/drawer_ecosystem_teams"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/drawer_ecosystem_talk"
app:layout_constraintTop_toTopOf="parent"
@@ -169,6 +169,43 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml
index ac2ed15dcfef..d5772a2b6b35 100644
--- a/app/src/main/res/values/dims.xml
+++ b/app/src/main/res/values/dims.xml
@@ -181,4 +181,5 @@
@dimen/standard_padding
@dimen/standard_padding
8dp
+ 240dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f3d6f60bf3e1..7b7267fdc1ed 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1481,6 +1481,45 @@
Notes
Talk
More
+ Teams
+ My teams
+ Other teams
+ Create team
+ Team name
+ No teams yet
+ Search teams
+ Clear search
+ No teams found
+ Resources
+ Members
+ Team settings
+ Nothing shared with this team yet
+ No members
+ Could not load team
+ Team not found
+ Add member
+ Search users, groups or email
+ Could not add member
+ You are not allowed to add members to this team
+ This member cannot be added to the team
+ Owner
+ Admin
+ Moderator
+ Member
+ Could not load teams
+ Could not create team
+
+ - Use %1$d to %2$d character
+ - Use %1$d to %2$d characters
+
+ This team name is not allowed
+ You are not allowed to create teams
+ Teams are not available on this server
+ Too many teams created in a short time. Try again in a minute.
+
+ - %d member
+ - %d members
+
Details
%s mm
ƒ/%s
diff --git a/gradle.properties b/gradle.properties
index 91f3dedc1c10..26e1baf5b5cf 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -23,8 +23,9 @@ org.gradle.configuration-cache=true
org.gradle.workers.max=4
# Needed for local libs
-# org.gradle.dependency.verification=off
-# systemProp.org.gradle.internal.publish.checksums.insecure=true
+
+org.gradle.dependency.verification=off
+systemProp.org.gradle.internal.publish.checksums.insecure=true
# Enabled parallel sync for Gradle 9.4+
org.gradle.tooling.parallel=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 32ddcc810ef1..d4c75fbb51cb 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[versions]
+activityComposeVersion = "1.13.0"
androidCommonLibraryVersion = "0.34.0"
androidGifDrawableVersion = "1.2.32"
androidImageCropperVersion = "4.7.0"
@@ -57,6 +58,7 @@ markwonVersion = "4.6.2"
materialIconsCoreVersion = "1.7.8"
materialVersion = "1.14.0"
media3 = "1.11.1"
+navigation3Version = "1.2.1"
mockitoKotlinVersion = "6.4.0"
mockitoVersion = "5.22.0"
mockkVersion = "1.14.12"
@@ -164,8 +166,15 @@ glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
# Lifecycle
lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycleViewmodelKtxVersion" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleViewmodelKtxVersion" }
+lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleViewmodelKtxVersion" }
lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycleViewmodelKtxVersion" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtxVersion" }
+lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelKtxVersion" }
+lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelKtxVersion" }
+
+# Navigation 3
+navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3Version" }
+navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3Version" }
# ksp
ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
@@ -177,7 +186,7 @@ compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
compose-material3 = { module = "androidx.compose.material3:material3" }
-compose-activity = { module = "androidx.activity:activity-compose" }
+compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activityComposeVersion" }
# Media3
media3-datasource = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index c0b04ea69bc9..2edf1fe4fa70 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -54,6 +54,7 @@
+
@@ -64,6 +65,7 @@
+
@@ -77,6 +79,7 @@
+
@@ -93,6 +96,7 @@
+
@@ -1353,6 +1357,9 @@
+
+
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 26e4b55708b4..6bc259761eec 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -43,14 +43,16 @@ dependencyResolutionManagement {
/*
Needed for local android library
+
+*/
+
+
includeBuild("../android-library") {
dependencySubstitution {
substitute(module("com.github.nextcloud:android-library"))
.using(project(":library"))
}
}
-*/
-
/*
Needed for local android common library