Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Expand Down
8 changes: 8 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,13 @@
android:resource="@xml/app_config" />

<activity
android:name="com.nextcloud.ui.composeActivity.ComposeActivity"
android:name="com.nextcloud.ui.compose.ComposeActivity"
android:exported="true"
android:label="@string/compose_activity_label"/>

<activity-alias
android:name="com.nextcloud.ui.composeActivity.ComposeProcessTextAlias"
android:targetActivity="com.nextcloud.ui.composeActivity.ComposeActivity"
android:name="com.nextcloud.ui.compose.ComposeProcessTextAlias"
android:targetActivity="com.nextcloud.ui.compose.ComposeActivity"
android:exported="true">

<intent-filter>
Expand Down Expand Up @@ -340,6 +340,10 @@
android:name=".ui.activity.TextEditorWebView"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="false" />
<activity
android:name="com.nextcloud.ui.teams.detail.TeamWebView"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="false" />
<activity
android:name=".ui.activity.SetupEncryptionActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.nextcloud.client.assistant

import android.app.Activity
import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -57,6 +58,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nextcloud.client.assistant.chat.ChatContent
import com.nextcloud.client.assistant.chat.ChatViewModel
import com.nextcloud.client.assistant.component.TaskSelectionAlertDialog
import com.nextcloud.client.assistant.conversation.ConversationScreen
import com.nextcloud.client.assistant.conversation.ConversationViewModel
import com.nextcloud.client.assistant.conversation.repository.MockConversationRemoteRepository
Expand All @@ -70,11 +72,10 @@ import com.nextcloud.client.assistant.task.TaskView
import com.nextcloud.client.assistant.taskTypes.TaskTypesRow
import com.nextcloud.client.assistant.translate.TranslationScreen
import com.nextcloud.client.assistant.translate.TranslationViewModel
import com.nextcloud.ui.composeActivity.ComposeActivity
import com.nextcloud.ui.composeActivity.ComposeViewModel
import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog
import com.nextcloud.ui.composeComponents.alertDialog.TaskSelectionAlertDialog
import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet
import com.nextcloud.ui.compose.ComposeActivity
import com.nextcloud.ui.compose.ComposeViewModel
import com.nextcloud.ui.compose.component.alertDialog.BaseAlertDialog
import com.nextcloud.ui.compose.component.bottomSheet.MoreActionsBottomSheet
import com.nextcloud.utils.extensions.getChat
import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.v2.model.Task
Expand Down Expand Up @@ -289,6 +290,7 @@ fun AssistantScreen(
val task = (screenState as AssistantScreenState.Translation).task
val textToTranslate = task?.input?.input ?: selectedText ?: ""

@SuppressLint("ViewModelConstructorInComposable")
val translationViewModel =
TranslationViewModel(remoteRepository = viewModel.getRemoteRepository())

Expand Down Expand Up @@ -415,12 +417,16 @@ private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewMod
state?.let {
when (state) {
is ScreenOverlayState.DeleteTask -> {
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 -> {
Expand Down Expand Up @@ -524,6 +530,7 @@ private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, description
}

@Suppress("MagicNumber")
@SuppressLint("ViewModelConstructorInComposable")
@Composable
@Preview
private fun AssistantScreenPreview() {
Expand All @@ -544,6 +551,7 @@ private fun AssistantScreenPreview() {
}

@Suppress("MagicNumber")
@SuppressLint("ViewModelConstructorInComposable")
@Composable
@Preview
private fun AssistantEmptyScreenPreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/nextcloud/client/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ 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
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
Expand Down Expand Up @@ -253,6 +254,9 @@ internal interface ComponentsModule {
@ContributesAndroidInjector
fun textEditorWebView(): TextEditorWebView

@ContributesAndroidInjector
fun teamWebView(): TeamWebView

@ContributesAndroidInjector
fun extendedListFragment(): ExtendedListFragment

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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
}
12 changes: 11 additions & 1 deletion app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -21,9 +23,17 @@ import javax.inject.Provider
* @see ViewModelKey
*/
class ViewModelFactory @Inject constructor(
private val viewModelProviders: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
private val viewModelProviders: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>,
private val savedStateViewModelCreators: Map<Class<out ViewModel>, @JvmSuppressWildcards SavedStateViewModelCreator>
) : ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val creator = savedStateViewModelCreators[modelClass] ?: return create(modelClass)

@Suppress("UNCHECKED_CAST")
return creator.create(extras.createSavedStateHandle()) as T
}

override fun <T : ViewModel> create(modelClass: Class<T>): T {
var vmProvider: Provider<ViewModel>? = viewModelProviders[modelClass]

Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
131 changes: 131 additions & 0 deletions app/src/main/java/com/nextcloud/ui/compose/ComposeActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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<View?>(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)
}
}
Loading
Loading