Skip to content

Commit b2a7ce4

Browse files
arlexTechrapterjet2004
authored andcommitted
- Refactoring addBubble for clarity and reducing nesting
- Refactoring createConversationBubble to use best practices, keeping ChatActivity.kt simple - Refactoring NotificationWorker functions related to bubbling to now properly follow the builder pattern - better error handling of edge cases - reimplementing UI in jetpack compose after rebase Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
1 parent 9a0999a commit b2a7ce4

16 files changed

Lines changed: 1452 additions & 99 deletions

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@
190190
android:name=".chat.ChatActivity"
191191
android:theme="@style/AppTheme" />
192192

193+
<activity
194+
android:name=".chat.BubbleActivity"
195+
android:theme="@style/AppTheme"
196+
android:allowEmbedded="true"
197+
android:resizeableActivity="true"
198+
android:documentLaunchMode="always" />
199+
193200
<activity
194201
android:name=".activities.CallActivity"
195202
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Alexandre Wery <nextcloud-talk-android@alwy.be>
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.chat
9+
10+
import android.content.Context
11+
import android.content.Intent
12+
import android.os.Bundle
13+
import androidx.activity.OnBackPressedCallback
14+
import com.nextcloud.talk.R
15+
import com.nextcloud.talk.activities.MainActivity
16+
import com.nextcloud.talk.utils.bundle.BundleKeys
17+
18+
class BubbleActivity : ChatActivity() {
19+
20+
override fun onCreate(savedInstanceState: Bundle?) {
21+
super.onCreate(savedInstanceState)
22+
supportActionBar?.setDisplayHomeAsUpEnabled(true)
23+
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_talk)
24+
supportActionBar?.setDisplayShowHomeEnabled(true)
25+
findViewById<androidx.appcompat.widget.Toolbar>(R.id.chat_toolbar)?.setNavigationOnClickListener {
26+
openConversationList()
27+
}
28+
29+
onBackPressedDispatcher.addCallback(
30+
this,
31+
object : OnBackPressedCallback(true) {
32+
override fun handleOnBackPressed() {
33+
moveTaskToBack(false)
34+
}
35+
}
36+
)
37+
}
38+
39+
override fun onPrepareOptionsMenu(menu: android.view.Menu): Boolean {
40+
super.onPrepareOptionsMenu(menu)
41+
42+
menu.findItem(R.id.create_conversation_bubble)?.isVisible = false
43+
menu.findItem(R.id.open_conversation_in_app)?.isVisible = true
44+
45+
return true
46+
}
47+
48+
override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean =
49+
when (item.itemId) {
50+
R.id.open_conversation_in_app -> {
51+
openInMainApp()
52+
true
53+
}
54+
android.R.id.home -> {
55+
openConversationList()
56+
true
57+
}
58+
else -> super.onOptionsItemSelected(item)
59+
}
60+
61+
private fun openInMainApp() {
62+
val intent = Intent(this, MainActivity::class.java).apply {
63+
action = Intent.ACTION_MAIN
64+
addCategory(Intent.CATEGORY_LAUNCHER)
65+
putExtras(this@BubbleActivity.intent)
66+
conversationUser?.id?.let { putExtra(BundleKeys.KEY_INTERNAL_USER_ID, it) }
67+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
68+
}
69+
startActivity(intent)
70+
}
71+
72+
private fun openConversationList() {
73+
val intent = Intent(this, MainActivity::class.java).apply {
74+
action = Intent.ACTION_MAIN
75+
addCategory(Intent.CATEGORY_LAUNCHER)
76+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
77+
}
78+
startActivity(intent)
79+
}
80+
81+
@Deprecated("Deprecated in Java")
82+
override fun onSupportNavigateUp(): Boolean {
83+
openInMainApp()
84+
return true
85+
}
86+
87+
companion object {
88+
fun newIntent(context: Context, roomToken: String, conversationName: String?): Intent =
89+
Intent(context, BubbleActivity::class.java).apply {
90+
putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken)
91+
conversationName?.let { putExtra(BundleKeys.KEY_CONVERSATION_NAME, it) }
92+
action = Intent.ACTION_VIEW
93+
flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
94+
}
95+
}
96+
}

app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ class ConversationInfoActivity : BaseActivity() {
167167
intent.getStringExtra(KEY_ROOM_TOKEN)
168168
) { "Missing room token" }
169169

170+
val shouldFocus = intent.getBooleanExtra(BundleKeys.KEY_FOCUS_CONVERSATION_BUBBLE, false)
171+
170172
val upcomingEvent = intent.getParcelableExtraProvider<UpcomingEvent>(BundleKeys.KEY_UPCOMING_EVENT)
171173
val upcomingEventSummary = upcomingEvent?.summary
172174
val upcomingEventTime = upcomingEvent?.start?.let { start ->
@@ -186,6 +188,7 @@ class ConversationInfoActivity : BaseActivity() {
186188
if (upcomingEventSummary != null || upcomingEventTime != null) {
187189
viewModel.setUpcomingEvent(upcomingEventSummary, upcomingEventTime)
188190
}
191+
viewModel.setFocusBubble(shouldFocus)
189192
}
190193
.onFailure {
191194
Log.e(TAG, "Failed to get current user")
@@ -325,7 +328,10 @@ class ConversationInfoActivity : BaseActivity() {
325328
onArchiveClick = { conversationUser?.let { viewModel.toggleArchive(it, conversationToken) } },
326329
onLeaveConversationClick = { leaveConversation() },
327330
onClearHistoryClick = { showClearHistoryDialog() },
328-
onDeleteConversationClick = { showDeleteConversationDialog() }
331+
onDeleteConversationClick = { showDeleteConversationDialog() },
332+
onBubbleClick = {
333+
viewModel.toggleBubble(this, this.lifecycleScope)
334+
}
329335
)
330336

331337
private fun showSharedItems() {

app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoUiState.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,11 @@ data class ConversationInfoUiState(
8181
val canDelete: Boolean = false,
8282
val showClearHistory: Boolean = false,
8383

84+
val showBubblesSetting: Boolean = false,
85+
val focusBubbleSetting: Boolean = false,
86+
val shouldBubble: Boolean = false,
87+
val forceAllBubbles: Boolean = false,
88+
val globalBubblesEnabled: Boolean = false,
89+
8490
val showEditButton: Boolean = false
8591
)

app/src/main/java/com/nextcloud/talk/conversationinfo/ui/ConversationInfoScreen.kt

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ package com.nextcloud.talk.conversationinfo.ui
1212
import android.content.res.Configuration
1313
import android.widget.ImageView
1414
import androidx.annotation.DrawableRes
15+
import androidx.compose.animation.animateColorAsState
16+
import androidx.compose.animation.core.tween
1517
import androidx.compose.foundation.background
18+
import androidx.compose.foundation.border
1619
import androidx.compose.foundation.clickable
1720
import androidx.compose.foundation.isSystemInDarkTheme
1821
import androidx.compose.foundation.layout.Arrangement
@@ -32,9 +35,11 @@ import androidx.compose.foundation.layout.padding
3235
import androidx.compose.foundation.layout.safeDrawing
3336
import androidx.compose.foundation.layout.size
3437
import androidx.compose.foundation.layout.width
38+
import androidx.compose.foundation.layout.wrapContentSize
3539
import androidx.compose.foundation.lazy.LazyColumn
3640
import androidx.compose.foundation.lazy.items
3741
import androidx.compose.foundation.shape.CircleShape
42+
import androidx.compose.foundation.shape.RoundedCornerShape
3843
import androidx.compose.material.icons.Icons
3944
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
4045
import androidx.compose.material3.Card
@@ -53,6 +58,7 @@ import androidx.compose.material3.TopAppBarDefaults
5358
import androidx.compose.material3.darkColorScheme
5459
import androidx.compose.material3.lightColorScheme
5560
import androidx.compose.runtime.Composable
61+
import androidx.compose.runtime.getValue
5662
import androidx.compose.runtime.remember
5763
import androidx.compose.ui.Alignment
5864
import androidx.compose.ui.Modifier
@@ -112,7 +118,8 @@ data class ConversationInfoScreenCallbacks(
112118
val onArchiveClick: () -> Unit = {},
113119
val onLeaveConversationClick: () -> Unit = {},
114120
val onClearHistoryClick: () -> Unit = {},
115-
val onDeleteConversationClick: () -> Unit = {}
121+
val onDeleteConversationClick: () -> Unit = {},
122+
val onBubbleClick: () -> Unit = {}
116123
)
117124

118125
@OptIn(ExperimentalMaterial3Api::class)
@@ -477,6 +484,41 @@ private fun NotificationSettingsSection(state: ConversationInfoUiState, callback
477484
onClick = callbacks.onCallNotificationsClick
478485
)
479486
}
487+
if (state.showBubblesSetting) {
488+
val bubbleSummaryText = when {
489+
!state.globalBubblesEnabled -> R.string.nc_conversation_notification_bubble_disabled
490+
state.forceAllBubbles -> R.string.nc_conversation_notification_bubble_forced
491+
else -> R.string.nc_conversation_notification_bubble_desc
492+
}
493+
494+
val highlightColor by animateColorAsState(
495+
targetValue = if (state.focusBubbleSetting) {
496+
MaterialTheme.colorScheme.primary
497+
} else {
498+
Color.Transparent
499+
},
500+
animationSpec = tween(durationMillis = 500)
501+
)
502+
503+
Box(
504+
modifier = Modifier
505+
.wrapContentSize()
506+
.alpha(if (state.globalBubblesEnabled && !state.forceAllBubbles) 1.0f else 0.5f)
507+
.border(
508+
width = 2.dp,
509+
color = highlightColor,
510+
shape = RoundedCornerShape(8.dp)
511+
)
512+
.padding(2.dp)
513+
) {
514+
SettingsRow(
515+
title = stringResource(R.string.nc_notification_settings_bubbles),
516+
subtitle = stringResource(bubbleSummaryText),
517+
checked = state.shouldBubble,
518+
onClick = callbacks.onBubbleClick
519+
)
520+
}
521+
}
480522
if (state.showSensitiveConversation) {
481523
SettingsRow(
482524
title = stringResource(R.string.nc_sensitive_conversation),

app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* SPDX-License-Identifier: GPL-3.0-or-later
66
*/
77
package com.nextcloud.talk.conversationinfo.viewmodel
8+
import android.content.Context
9+
import android.os.Build
810
import android.util.Log
911
import androidx.lifecycle.DefaultLifecycleObserver
1012
import androidx.lifecycle.LifecycleOwner
@@ -22,6 +24,7 @@ import com.nextcloud.talk.conversationinfo.CreateRoomRequest
2224
import com.nextcloud.talk.conversationinfo.Participants
2325
import com.nextcloud.talk.conversationinfo.model.ParticipantModel
2426
import com.nextcloud.talk.data.user.model.User
27+
import com.nextcloud.talk.jobs.NotificationWorker.Companion.BUBBLE_SWITCH_KEY
2528
import com.nextcloud.talk.models.domain.ConversationModel
2629
import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelConverter
2730
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@@ -46,12 +49,15 @@ import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
4649
import com.nextcloud.talk.utils.ConversationUtils
4750
import com.nextcloud.talk.utils.DateConstants
4851
import com.nextcloud.talk.utils.DisplayUtils
52+
import com.nextcloud.talk.utils.NotificationUtils
4953
import com.nextcloud.talk.utils.SpreedFeatures
54+
import com.nextcloud.talk.utils.preferences.AppPreferences
5055
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
5156
import io.reactivex.Observer
5257
import io.reactivex.android.schedulers.AndroidSchedulers
5358
import io.reactivex.disposables.Disposable
5459
import io.reactivex.schedulers.Schedulers
60+
import kotlinx.coroutines.CoroutineScope
5561
import kotlinx.coroutines.flow.MutableSharedFlow
5662
import kotlinx.coroutines.flow.MutableStateFlow
5763
import kotlinx.coroutines.flow.SharedFlow
@@ -73,7 +79,8 @@ import javax.inject.Inject
7379
class ConversationInfoViewModel @Inject constructor(
7480
private val chatNetworkDataSource: ChatNetworkDataSource,
7581
private val conversationsRepository: ConversationsRepository,
76-
private val ncApi: NcApi
82+
private val ncApi: NcApi,
83+
private val appPreferences: AppPreferences
7784
) : ViewModel() {
7885
object LifeCycleObserver : DefaultLifecycleObserver {
7986
enum class LifeCycleFlag {
@@ -417,6 +424,12 @@ class ConversationInfoViewModel @Inject constructor(
417424
val showMessageExpiration = isModerator &&
418425
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)
419426

427+
val globalBubblesEnabled = appPreferences.areBubblesEnabled()
428+
val forceAllBubbles = appPreferences.areBubblesForced()
429+
val showBubblesSetting = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
430+
431+
val shouldBubble = globalBubblesEnabled && (forceAllBubbles || dbModule.getBoolean(BUBBLE_SWITCH_KEY, false))
432+
420433
val credentials = ApiUtils.getCredentials(user.username, user.token) ?: ""
421434

422435
_uiState.update { state ->
@@ -467,6 +480,10 @@ class ConversationInfoViewModel @Inject constructor(
467480
canLeave = canLeave,
468481
canDelete = canDelete,
469482
showClearHistory = showClearHistory,
483+
showBubblesSetting = showBubblesSetting,
484+
forceAllBubbles = forceAllBubbles,
485+
shouldBubble = shouldBubble,
486+
globalBubblesEnabled = globalBubblesEnabled,
470487
showEditButton = showEditButton
471488
)
472489
}
@@ -813,6 +830,33 @@ class ConversationInfoViewModel @Inject constructor(
813830
_uiState.update { it.copy(upcomingEventSummary = summary, upcomingEventTime = time) }
814831
}
815832

833+
fun setFocusBubble(focus: Boolean) {
834+
_uiState.update { it.copy(focusBubbleSetting = focus) }
835+
}
836+
837+
fun toggleBubble(context: Context, scope: CoroutineScope) {
838+
val rowIsInteractive = uiState.value.globalBubblesEnabled && !uiState.value.forceAllBubbles
839+
840+
if (!rowIsInteractive) {
841+
return
842+
}
843+
844+
val newValue = !uiState.value.shouldBubble
845+
_uiState.update { it.copy(shouldBubble = newValue) }
846+
847+
scope.launch {
848+
databaseStorageModule?.saveBoolean(BUBBLE_SWITCH_KEY, newValue)
849+
}
850+
851+
if (!newValue) {
852+
NotificationUtils.dismissBubbleForRoom(
853+
context,
854+
currentUser ?: return,
855+
currentToken
856+
)
857+
}
858+
}
859+
816860
suspend fun emitSnackbar(@androidx.annotation.StringRes resId: Int) {
817861
_uiEvent.emit(ConversationInfoUiEvent.ShowSnackbar(resId))
818862
}

0 commit comments

Comments
 (0)