Skip to content

Commit 78b1e17

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 b7c1bc1 commit 78b1e17

17 files changed

Lines changed: 1551 additions & 108 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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
26+
findViewById<androidx.appcompat.widget.Toolbar>(R.id.chatToolbarComposeView)?.setNavigationOnClickListener {
27+
openConversationList()
28+
}
29+
30+
onBackPressedDispatcher.addCallback(
31+
this,
32+
object : OnBackPressedCallback(true) {
33+
override fun handleOnBackPressed() {
34+
moveTaskToBack(false)
35+
}
36+
}
37+
)
38+
}
39+
40+
override fun onPrepareOptionsMenu(menu: android.view.Menu): Boolean {
41+
super.onPrepareOptionsMenu(menu)
42+
43+
menu.findItem(R.id.create_conversation_bubble)?.isVisible = false
44+
menu.findItem(R.id.open_conversation_in_app)?.isVisible = true
45+
46+
return true
47+
}
48+
49+
override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean =
50+
when (item.itemId) {
51+
R.id.open_conversation_in_app -> {
52+
openInMainApp()
53+
true
54+
}
55+
android.R.id.home -> {
56+
openConversationList()
57+
true
58+
}
59+
else -> super.onOptionsItemSelected(item)
60+
}
61+
62+
private fun openInMainApp() {
63+
val intent = Intent(this, MainActivity::class.java).apply {
64+
action = Intent.ACTION_MAIN
65+
addCategory(Intent.CATEGORY_LAUNCHER)
66+
putExtras(this@BubbleActivity.intent)
67+
conversationUser?.id?.let { putExtra(BundleKeys.KEY_INTERNAL_USER_ID, it) }
68+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
69+
}
70+
startActivity(intent)
71+
}
72+
73+
private fun openConversationList() {
74+
val intent = Intent(this, MainActivity::class.java).apply {
75+
action = Intent.ACTION_MAIN
76+
addCategory(Intent.CATEGORY_LAUNCHER)
77+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
78+
}
79+
startActivity(intent)
80+
}
81+
82+
@Deprecated("Deprecated in Java")
83+
override fun onSupportNavigateUp(): Boolean {
84+
openInMainApp()
85+
return true
86+
}
87+
88+
companion object {
89+
fun newIntent(context: Context, roomToken: String, conversationName: String?): Intent =
90+
Intent(context, BubbleActivity::class.java).apply {
91+
putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken)
92+
conversationName?.let { putExtra(BundleKeys.KEY_CONVERSATION_NAME, it) }
93+
action = Intent.ACTION_VIEW
94+
flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
95+
}
96+
}
97+
}

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Nextcloud Talk - Android Client
33
*
4+
* SPDX-FileCopyrightText: 2025 Alexandre Wery <nextcloud-talk-android@alwy.be>
45
* SPDX-FileCopyrightText: 2024 Christian Reiner <foss@christian-reiner.info>
56
* SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
67
* SPDX-FileCopyrightText: 2024 Giacomo Pacini <giacomo@paciosoft.com>
@@ -127,6 +128,7 @@ import com.nextcloud.talk.events.UserMentionClickEvent
127128
import com.nextcloud.talk.events.WebSocketCommunicationEvent
128129
import com.nextcloud.talk.jobs.DeleteConversationWorker
129130
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
131+
import com.nextcloud.talk.jobs.NotificationWorker.Companion.BUBBLE_SWITCH_KEY
130132
import com.nextcloud.talk.jobs.ShareOperationWorker
131133
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
132134
import com.nextcloud.talk.location.LocationPickerActivity
@@ -142,6 +144,7 @@ import com.nextcloud.talk.models.json.threads.ThreadInfo
142144
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
143145
import com.nextcloud.talk.polls.ui.PollMainDialogFragment
144146
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
147+
import com.nextcloud.talk.settings.SettingsActivity
145148
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
146149
import com.nextcloud.talk.signaling.SignalingMessageReceiver
147150
import com.nextcloud.talk.signaling.SignalingMessageSender
@@ -198,6 +201,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWIT
198201
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
199202
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
200203
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
204+
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
201205
import com.nextcloud.talk.utils.rx.DisposableSet
202206
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
203207
import com.nextcloud.talk.webrtc.Globals
@@ -241,7 +245,7 @@ import kotlin.math.abs
241245

242246
@Suppress("TooManyFunctions", "LargeClass", "LongMethod")
243247
@AutoInjector(NextcloudTalkApplication::class)
244-
class ChatActivity :
248+
open class ChatActivity :
245249
BaseActivity(),
246250
CallStartedMessageInterface {
247251

@@ -1196,8 +1200,8 @@ class ChatActivity :
11961200

11971201
pendingTargetMessageId = extras?.getString(BundleKeys.KEY_MESSAGE_ID)?.toLongOrNull()?.takeIf { it > 0L }
11981202
?: extras?.getLong(BundleKeys.KEY_MESSAGE_ID)?.takeIf { it > 0L }
1199-
pendingTargetThreadId = extras?.getString(BundleKeys.KEY_THREAD_ID)?.toLongOrNull()?.takeIf { it > 0L }
1200-
?: extras?.getLong(BundleKeys.KEY_THREAD_ID)?.takeIf { it > 0L }
1203+
pendingTargetThreadId = extras?.getString(KEY_THREAD_ID)?.toLongOrNull()?.takeIf { it > 0L }
1204+
?: extras?.getLong(KEY_THREAD_ID)?.takeIf { it > 0L }
12011205
pendingTargetSearchQuery = extras?.getString(BundleKeys.KEY_SEARCH_QUERY)
12021206
}
12031207

@@ -2033,6 +2037,12 @@ class ChatActivity :
20332037
onClick = { openScheduledMessages() }
20342038
)
20352039
}
2040+
if (NotificationUtils.deviceSupportsBubbles) {
2041+
items += MenuItemData(
2042+
title = getString(R.string.nc_create_bubble),
2043+
onClick = { createConversationBubble() }
2044+
)
2045+
}
20362046
}
20372047
if (currentConversation?.objectType == ConversationEnums.ObjectType.FILE) {
20382048
items += MenuItemData(
@@ -2275,8 +2285,10 @@ class ChatActivity :
22752285

22762286
private fun setupChatEmptyStateView() {
22772287
binding.chatEmptyStateComposeView.setContent {
2278-
val type by chatEmptyStateType
2279-
type?.let { ChatEmptyState(it) }
2288+
MaterialTheme(colorScheme = viewThemeUtils.getColorScheme(this@ChatActivity)) {
2289+
val type by chatEmptyStateType
2290+
type?.let { ChatEmptyState(it) }
2291+
}
22802292
}
22812293
}
22822294

@@ -2308,6 +2320,79 @@ class ChatActivity :
23082320
chatEmptyStateType.value = ChatEmptyStateType.Lobby(sb.toString())
23092321
}
23102322

2323+
private fun openBubbleSettings() {
2324+
val intent = Intent(this, SettingsActivity::class.java)
2325+
intent.putExtra(BundleKeys.KEY_FOCUS_BUBBLE_SETTINGS, true)
2326+
startActivity(intent)
2327+
}
2328+
2329+
@Suppress("ReturnCount")
2330+
private fun createConversationBubble() {
2331+
if (!NotificationUtils.deviceSupportsBubbles) {
2332+
Log.e(
2333+
TAG,
2334+
"createConversationBubble was called but device doesn't support it. It should not be possible " +
2335+
"to get here via UI!"
2336+
)
2337+
return
2338+
}
2339+
2340+
if (!appPreferences.areBubblesEnabled() || !NotificationUtils.areSystemBubblesEnabled(context)) {
2341+
// Do not replace with snackbar as it needs to survive screen change
2342+
Toast.makeText(
2343+
context,
2344+
getString(R.string.nc_conversation_notification_bubble_disabled),
2345+
Toast.LENGTH_SHORT
2346+
).show()
2347+
openBubbleSettings()
2348+
return
2349+
}
2350+
2351+
if (!appPreferences.areBubblesForced() && !isConversationBubbleEnabled()) {
2352+
// Do not replace with snackbar as it needs to survive screen change
2353+
Toast.makeText(
2354+
context,
2355+
getString(R.string.nc_conversation_notification_bubble_enable_conversation),
2356+
Toast.LENGTH_SHORT
2357+
).show()
2358+
showConversationInfoScreen(focusBubbleSwitch = true)
2359+
return
2360+
}
2361+
2362+
val conversationName = currentConversation?.displayName ?: getString(R.string.nc_app_name)
2363+
currentConversation?.let {
2364+
val bubbleInfo = NotificationUtils.BubbleInfo(
2365+
roomToken = roomToken,
2366+
conversationRemoteId = it.name,
2367+
conversationName = conversationName,
2368+
conversationUser = conversationUser,
2369+
isOneToOneConversation = isOneToOneConversation(),
2370+
credentials = credentials
2371+
)
2372+
2373+
NotificationUtils.createConversationBubble(
2374+
context = context,
2375+
bubbleInfo = bubbleInfo,
2376+
appPreferences = appPreferences,
2377+
lifecycleScope
2378+
)
2379+
}
2380+
}
2381+
2382+
private fun isConversationBubbleEnabled(): Boolean =
2383+
runCatching {
2384+
DatabaseStorageModule(conversationUser, roomToken).getBoolean(BUBBLE_SWITCH_KEY, false)
2385+
}.onFailure { e ->
2386+
when (e) {
2387+
is IOException -> Log.e(TAG, "Failed to read conversation bubble preference: IO error", e)
2388+
is IllegalStateException -> Log.e(
2389+
TAG,
2390+
"Failed to read conversation bubble preference: Invalid state",
2391+
e
2392+
)
2393+
}
2394+
}.getOrDefault(false)
2395+
23112396
private fun onRemoteFileBrowsingResult(intent: Intent?) {
23122397
val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
23132398
if (pathList?.size!! >= 1) {
@@ -2700,11 +2785,14 @@ class ChatActivity :
27002785
)
27012786
}
27022787

2703-
private fun showConversationInfoScreen() {
2788+
private fun showConversationInfoScreen(focusBubbleSwitch: Boolean = false) {
27042789
val bundle = Bundle()
27052790

27062791
bundle.putString(KEY_ROOM_TOKEN, roomToken)
27072792
bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
2793+
if (focusBubbleSwitch) {
2794+
bundle.putBoolean(BundleKeys.KEY_FOCUS_CONVERSATION_BUBBLE, true)
2795+
}
27082796

27092797
val upcomingEvent =
27102798
(chatViewModel.upcomingEventViewState.value as? ChatViewModel.UpcomingEventUIState.Success)?.event
@@ -2723,9 +2811,10 @@ class ChatActivity :
27232811
sessionIdAfterRoomJoined != "0"
27242812

27252813
@Suppress("Detekt.TooGenericExceptionCaught")
2726-
private fun cancelNotificationsForCurrentConversation() {
2814+
protected open fun cancelNotificationsForCurrentConversation() {
2815+
val isBubbleMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isLaunchedFromBubble
27272816
if (conversationUser != null) {
2728-
if (!TextUtils.isEmpty(roomToken)) {
2817+
if (!TextUtils.isEmpty(roomToken) && !isBubbleMode) {
27292818
try {
27302819
NotificationUtils.cancelExistingNotificationsForRoom(
27312820
applicationContext,
@@ -3632,7 +3721,7 @@ class ChatActivity :
36323721
val lon = message.geoLocationParameters.longitude
36333722
metaData =
36343723
"{\"type\":\"geo-location\",\"id\":\"geo:$lat,$lon\",\"latitude\":\"$lat\"," +
3635-
"\"longitude\":\"$lon\",\"name\":\"$name\"}"
3724+
"\"longitude\":\"$lon\",\"name\":\"$name\"}"
36363725
}
36373726

36383727
shareToNotes(shareUri, noteToSelfConversation.token, message, objectId, metaData)

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
)

0 commit comments

Comments
 (0)