From 9c879e913ae55355c48093db316abc610674fb4d Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Sun, 15 Feb 2026 12:12:33 +0100 Subject: [PATCH 1/2] feat(ecosystem): Add ecosystem bar to account chooser dialog Resolves: #3248 Signed-off-by: Andy Scherzinger --- app/build.gradle | 1 + .../com/nextcloud/talk/chat/ChatActivity.kt | 1 + .../ConversationsListActivity.kt | 7 +- .../viewmodels/ConversationsListViewModel.kt | 1 + .../talk/settings/SettingsActivity.kt | 21 + .../ui/dialog/ChooseAccountDialogCompose.kt | 165 +++++++- .../utils/preferences/AppPreferences.java | 6 + .../utils/preferences/AppPreferencesImpl.kt | 17 + app/src/main/res/drawable/ic_more_apps.xml | 27 ++ app/src/main/res/drawable/ic_notes.xml | 18 + app/src/main/res/layout/activity_settings.xml | 393 ++++++++++-------- app/src/main/res/values/setup.xml | 2 + app/src/main/res/values/strings.xml | 6 + detekt.yml | 2 +- gradle/verification-metadata.xml | 3 + 15 files changed, 477 insertions(+), 193 deletions(-) create mode 100644 app/src/main/res/drawable/ic_more_apps.xml create mode 100644 app/src/main/res/drawable/ic_notes.xml diff --git a/app/build.gradle b/app/build.gradle index a2424e32584..0ea911f0385 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -315,6 +315,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.17.0' implementation 'androidx.activity:activity-ktx:1.12.4' implementation 'com.github.nextcloud.android-common:ui:0.33.0' + implementation 'com.github.nextcloud.android-common:core:0.33.0' implementation 'com.github.nextcloud-deps:android-talk-webrtc:132.6834.0' gplayImplementation 'com.google.android.gms:play-services-base:18.10.0' diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 086c806cc13..ef978b9b843 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -4313,6 +4313,7 @@ class ChatActivity : } } + @Suppress("Detekt.TooGenericExceptionCaught") private fun shareToNotes( shareUri: Uri?, roomToken: String, diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 402b266a0b2..27050f040d2 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -756,10 +756,15 @@ class ConversationsListActivity : } private fun showChooseAccountDialog() { + val brandedClient = getResources().getBoolean(R.bool.is_branded_client) binding.genericComposeView.apply { val shouldDismiss = mutableStateOf(false) setContent { - ChooseAccountDialogCompose().GetChooseAccountDialog(shouldDismiss, this@ConversationsListActivity) + ChooseAccountDialogCompose().GetChooseAccountDialog( + shouldDismiss, + this@ConversationsListActivity, + appPreferences.isShowEcosystem && !brandedClient + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index 8be8943739a..316efb57afe 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -281,6 +281,7 @@ class ConversationsListViewModel @Inject constructor( return differenceMillis > checkIntervalInMillies } + @Suppress("Detekt.TooGenericExceptionCaught") fun checkIfFollowedThreadsExist() { val threadsUrl = ApiUtils.getUrlForSubscribedThreads( version = 1, diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt index 1de5203983a..ef0089b93fc 100644 --- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt @@ -216,6 +216,7 @@ class SettingsActivity : } setupCheckables(isOnline.value) + setupEcosystemSetting() setupScreenLockSetting() setupNotificationSettings() setupProxyTypeSettings() @@ -252,6 +253,14 @@ class SettingsActivity : } } + private fun setupEcosystemSetting() { + if (getResources().getBoolean(R.bool.is_branded_client)) { + binding.settingsShowEcosystem.visibility = View.GONE + } else { + binding.settingsShowEcosystem.visibility = View.VISIBLE + } + } + @Suppress("MagicNumber") private fun scrollToNotificationCategory() { binding.scrollView.post { @@ -771,6 +780,7 @@ class SettingsActivity : private fun themeSwitchPreferences() { binding.run { listOf( + settingsShowEcosystemSwitch, settingsShowNotificationWarningSwitch, settingsScreenLockSwitch, settingsScreenSecuritySwitch, @@ -967,6 +977,8 @@ class SettingsActivity : } private fun setupCheckables(isOnline: Boolean) { + setupShowEcosystemCheckable() + binding.settingsShowNotificationWarningSwitch.isChecked = appPreferences.showRegularNotificationWarning @@ -1031,6 +1043,15 @@ class SettingsActivity : } } + private fun setupShowEcosystemCheckable() { + binding.settingsShowEcosystemSwitch.isChecked = appPreferences.isShowEcosystem + binding.settingsShowEcosystem.setOnClickListener { + val isChecked = binding.settingsShowEcosystemSwitch.isChecked + binding.settingsShowEcosystemSwitch.isChecked = !isChecked + appPreferences.setShowEcosystem(!isChecked) + } + } + private fun setupPhoneBookIntegrationSetting() { binding.settingsPhoneBookIntegrationSwitch.isChecked = appPreferences.isPhoneBookIntegrationEnabled binding.settingsPhoneBookIntegration.setOnClickListener { diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogCompose.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogCompose.kt index 71b5c26efff..6b666f3d7cf 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogCompose.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogCompose.kt @@ -10,10 +10,13 @@ package com.nextcloud.talk.ui.dialog import android.app.Activity import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.util.Log import android.widget.ImageView import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -21,12 +24,16 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -45,6 +52,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource @@ -59,6 +67,9 @@ import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import autodagger.AutoInjector import coil.compose.AsyncImage +import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp +import com.nextcloud.android.common.core.utils.ecosystem.EcosystemManager +import com.nextcloud.android.common.core.utils.ecosystem.LinkHelper import com.nextcloud.talk.R import com.nextcloud.talk.account.ServerSelectionActivity import com.nextcloud.talk.account.data.model.AccountItem @@ -110,11 +121,13 @@ class ChooseAccountDialogCompose { @Inject lateinit var networkMonitor: NetworkMonitor + lateinit var ecosystemManager: EcosystemManager + private val userItems = mutableStateListOf() @Composable @Suppress("LongMethod") - fun GetChooseAccountDialog(shouldDismiss: MutableState, activity: Activity) { + fun GetChooseAccountDialog(shouldDismiss: MutableState, activity: Activity, showEcosystem: Boolean) { if (shouldDismiss.value) return val colorScheme = viewThemeUtils.getColorScheme(activity) val status = remember { mutableStateOf(null) } @@ -124,6 +137,7 @@ class ChooseAccountDialogCompose { val isOnline by networkMonitor.isOnline.collectAsState() val currentUser = currentUserProvider.currentUser.blockingGet()!! val isStatusAvailable = CapabilitiesUtil.isUserStatusAvailable(currentUser) + ecosystemManager = EcosystemManager(activity) LaunchedEffect(currentUser) { val users = userManager.users.blockingGet() @@ -167,12 +181,25 @@ class ChooseAccountDialogCompose { shouldDismiss.value = true openSettings(activity) }, + onEcosystemFilesClick = { + shouldDismiss.value = true + openEcosystemFiles(currentUser) + }, + onEcosystemNotesClick = { + shouldDismiss.value = true + openEcosystemNotes(currentUser) + }, + onEcosystemMoreClick = { + shouldDismiss.value = true + openEcosystemMore(activity) + }, accountRowContent = { user -> AccountRow(user, activity) { shouldDismiss.value = true } }, statusIndicator = { modifier -> StatusIndicator(modifier = modifier, status = status.value, context = context) }, + showEcosystem = showEcosystem, context = context ) } @@ -229,6 +256,20 @@ class ChooseAccountDialogCompose { activity.startActivity(intent) } + private fun openEcosystemFiles(currentUser: User) { + ecosystemManager.openApp(EcosystemApp.FILES, getAccountHandle(currentUser)) + } + + private fun openEcosystemNotes(currentUser: User) { + ecosystemManager.openApp(EcosystemApp.NOTES, getAccountHandle(currentUser)) + } + + private fun getAccountHandle(user: User): String = user.username + "@" + user.baseUrl?.toUri()?.host + + private fun openEcosystemMore(activity: Activity) { + LinkHelper.openAppStore("Nextcloud", true, activity) + } + @Composable private fun AccountRow(userItem: AccountItem, activity: Activity, onSelected: () -> Unit) { Row( @@ -258,7 +299,11 @@ class ChooseAccountDialogCompose { .size(40.dp) .clip(CircleShape) ) - Column(modifier = Modifier.padding(start = 12.dp).weight(1f)) { + Column( + modifier = Modifier + .padding(start = 12.dp) + .weight(1f) + ) { Text( text = userItem.user.displayName ?: userItem.user.username ?: "" ) @@ -306,6 +351,7 @@ class ChooseAccountDialogCompose { } } } + companion object { private const val STATUS_SIZE_DP = 9f private val TAG = ChooseAccountDialogCompose::class.simpleName @@ -331,8 +377,12 @@ private fun ChooseAccountDialogContent( onSetStatusMessageClick: () -> Unit, onAddAccountClick: () -> Unit, onOpenSettingsClick: () -> Unit, + onEcosystemFilesClick: () -> Unit, + onEcosystemNotesClick: () -> Unit, + onEcosystemMoreClick: () -> Unit, accountRowContent: @Composable (AccountItem) -> Unit, statusIndicator: @Composable (Modifier) -> Unit, + showEcosystem: Boolean = true, context: Context ) { Dialog(onDismissRequest = { shouldDismiss.value = true }) { @@ -358,7 +408,21 @@ private fun ChooseAccountDialogContent( HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp) ) - LazyColumn(modifier = Modifier.padding(start = 8.dp).weight(1f, fill = false)) { + if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT && showEcosystem) { + EcosystemAppsSection( + onFilesClick = onEcosystemFilesClick, + onNotesClick = onEcosystemNotesClick, + onMoreClick = onEcosystemMoreClick + ) + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp) + ) + } + LazyColumn( + modifier = Modifier + .padding(start = 8.dp) + .weight(1f, fill = false) + ) { items(accountItems) { account -> if (account.user.userId + account.user.baseUrl != currentUser.userId + currentUser.baseUrl) { accountRowContent(account) @@ -407,7 +471,8 @@ private fun CurrentUserSection( statusIndicator(Modifier.align(Alignment.BottomEnd)) } Column( - modifier = Modifier.padding(start = 12.dp) + modifier = Modifier + .padding(start = 12.dp) .weight(1f) ) { Text(text = currentUser.displayName ?: currentUser.username ?: "") @@ -444,6 +509,69 @@ private fun CurrentUserSection( } } +@Composable +private fun EcosystemAppsSection(onFilesClick: () -> Unit, onNotesClick: () -> Unit, onMoreClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + EcosystemAppItem( + iconRes = R.drawable.ic_mimetype_folder, + label = stringResource(R.string.ecosystem_apps_files), + contentDescription = stringResource(R.string.ecosystem_apps_files), + onClick = onFilesClick + ) + EcosystemAppItem( + iconRes = R.drawable.ic_notes, + label = stringResource(R.string.ecosystem_apps_notes), + contentDescription = stringResource(R.string.ecosystem_apps_notes), + onClick = onNotesClick + ) + EcosystemAppItem( + iconRes = R.drawable.ic_more_apps, + label = stringResource(R.string.ecosystem_apps_more), + contentDescription = stringResource(R.string.ecosystem_apps_more), + onClick = onMoreClick + ) + } +} + +@Composable +private fun EcosystemAppItem(iconRes: Int, label: String, contentDescription: String, onClick: () -> Unit) { + Column( + modifier = Modifier + .widthIn(max = 80.dp) + .clickable(onClick = onClick) + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = contentDescription, + modifier = Modifier + .size(40.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = CircleShape + ) + .padding(8.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + Text( + text = label, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(top = 4.dp), + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + @Composable private fun StatusActionButtons(onSetOnlineStatusClick: () -> Unit, onSetStatusMessageClick: () -> Unit) { Row { @@ -482,7 +610,9 @@ private fun OnlineActions(onAddAccountClick: () -> Unit, onOpenSettingsClick: () if (isOnline) { TextButton(onClick = onAddAccountClick, modifier = Modifier.fillMaxWidth()) { Row( - modifier = Modifier.padding(start = 16.dp, top = 8.dp).fillMaxWidth(), + modifier = Modifier + .padding(start = 16.dp, top = 8.dp) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -502,7 +632,9 @@ private fun OnlineActions(onAddAccountClick: () -> Unit, onOpenSettingsClick: () TextButton(onClick = onOpenSettingsClick, modifier = Modifier.fillMaxWidth()) { Row( - modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 16.dp).fillMaxWidth(), + modifier = Modifier + .padding(start = 16.dp, top = 8.dp, bottom = 16.dp) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -520,7 +652,17 @@ private fun OnlineActions(onAddAccountClick: () -> Unit, onOpenSettingsClick: () } } -@Preview(showBackground = true) +@Preview(name = "Light Mode", showBackground = true) +@Preview( + name = "Dark Mode", + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL +) +@Preview( + name = "R-t-L", + showBackground = true, + locale = "ar" +) @Composable private fun ChooseAccountDialogContentPreview() { val shouldDismiss = remember { mutableStateOf(false) } @@ -540,11 +682,13 @@ private fun ChooseAccountDialogContentPreview() { status = "online", statusIsUserDefined = true ) - MaterialTheme { + val isDark = isSystemInDarkTheme() + val colorScheme = if (isDark) darkColorScheme() else lightColorScheme() + MaterialTheme(colorScheme = colorScheme) { val context = LocalContext.current ChooseAccountDialogContent( shouldDismiss = shouldDismiss, - colorScheme = MaterialTheme.colorScheme, + colorScheme = colorScheme, currentUser = sampleUser, status = sampleStatus, isStatusAvailable = true, @@ -555,6 +699,9 @@ private fun ChooseAccountDialogContentPreview() { onSetStatusMessageClick = {}, onAddAccountClick = {}, onOpenSettingsClick = {}, + onEcosystemFilesClick = {}, + onEcosystemNotesClick = {}, + onEcosystemMoreClick = {}, accountRowContent = {}, statusIndicator = { modifier -> SampleStatusIndicator(modifier) }, context = context diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index 43f0e5e9651..8dea169ad58 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -116,6 +116,12 @@ public interface AppPreferences { void removeIncognitoKeyboard(); + boolean getIsShowEcosystem(); + + void setShowEcosystem(boolean value); + + void removeShowEcosystem(); + boolean isPhoneBookIntegrationEnabled(); void setPhoneBookIntegration(boolean value); diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt index adc8fb1f35e..16de5234d5a 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt @@ -301,6 +301,22 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { setIncognitoKeyboard(false) } + override fun getIsShowEcosystem(): Boolean { + val read = runBlocking { async { readBoolean(SHOW_ECOSYSTEM, true).first() } }.getCompleted() + return read + } + + override fun setShowEcosystem(value: Boolean) = + runBlocking { + async { + writeBoolean(SHOW_ECOSYSTEM, value) + } + } + + override fun removeShowEcosystem() { + setShowEcosystem(false) + } + override fun isPhoneBookIntegrationEnabled(): Boolean = runBlocking { async { readBoolean(PHONE_BOOK_INTEGRATION).first() } @@ -607,6 +623,7 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { const val SCREEN_SECURITY = "screen_security" const val SCREEN_LOCK = "screen_lock" const val INCOGNITO_KEYBOARD = "incognito_keyboard" + const val SHOW_ECOSYSTEM = "SHOW_ECOSYSTEM" const val PHONE_BOOK_INTEGRATION = "phone_book_integration" const val LINK_PREVIEWS = "link_previews" const val SCREEN_LOCK_TIMEOUT = "screen_lock_timeout" diff --git a/app/src/main/res/drawable/ic_more_apps.xml b/app/src/main/res/drawable/ic_more_apps.xml new file mode 100644 index 00000000000..b5c11ed15c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_apps.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_notes.xml b/app/src/main/res/drawable/ic_notes.xml new file mode 100644 index 00000000000..bd5b22a6059 --- /dev/null +++ b/app/src/main/res/drawable/ic_notes.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index a3191b81575..d9511c644a4 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,5 +1,4 @@ - - + false https://nextcloud.com/privacy/ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a02854a70ec..dc9685787aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -172,6 +172,10 @@ How to translate with transifex: Add account Active user + Files + Notes + More + Personal Info Diagnosis @@ -935,4 +939,6 @@ How to translate with transifex: Send later without notification In %1$s No connection to server - Scheduled messages could not be loaded + Show app switcher + Nextcloud app suggestions in account chooser dialog diff --git a/detekt.yml b/detekt.yml index 7e293c8f900..a516d586a5c 100644 --- a/detekt.yml +++ b/detekt.yml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: GPL-3.0-or-later build: - maxIssues: 81 + maxIssues: 80 weights: # complexity: 2 # LongParameterList: 1 diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ad2df26d837..e13e3d045cb 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -27964,6 +27964,9 @@ + + + From 01e2e3c7558832b1b09e2d4a1e80ee524248f7bd Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Tue, 17 Feb 2026 20:48:08 +0100 Subject: [PATCH 2/2] fix(eco): Add Intent to be launched by ecosystem app bars Signed-off-by: Andy Scherzinger --- app/src/main/AndroidManifest.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a6ef2dc6bb5..fc0c1a7d67c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -110,6 +110,11 @@ + + + + +