Skip to content

Commit 279d9a4

Browse files
committed
feat(fc): update active notification on direct reply
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 906abe0 commit 279d9a4

6 files changed

Lines changed: 301 additions & 225 deletions

File tree

flipchatApp/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,15 @@
109109
</service>
110110

111111
<service
112-
android:name=".services.FcNotificationService"
112+
android:name=".notifications.FcNotificationService"
113113
android:exported="false">
114114
<intent-filter>
115115
<action android:name="com.google.firebase.MESSAGING_EVENT" />
116116
</intent-filter>
117117
</service>
118118

119119
<receiver
120-
android:name=".services.FcNotificationReceiver"
120+
android:name=".notifications.FcNotificationReceiver"
121121
android:enabled="true"
122122
android:exported="false" />
123123

flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/PurchaseAccountScreen.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ private class PurchaseAccountViewModel @Inject constructor(
228228
is IapPaymentEvent.OnSuccess -> event
229229
}
230230
}.filterIsInstance<IapPaymentEvent.OnSuccess>()
231-
232231
.onEach {
233232
dispatchEvent(Event.OnCreatingChanged(true))
234233
authManager.register(userManager.displayName!!)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package xyz.flipchat.app.notifications
2+
3+
import android.annotation.SuppressLint
4+
import android.app.Notification
5+
import android.content.BroadcastReceiver
6+
import android.content.Context
7+
import android.content.Intent
8+
import androidx.core.app.NotificationCompat
9+
import androidx.core.app.NotificationManagerCompat
10+
import androidx.core.app.Person
11+
import androidx.core.app.RemoteInput
12+
import com.getcode.vendor.Base58
13+
import dagger.hilt.android.AndroidEntryPoint
14+
import kotlinx.coroutines.CoroutineScope
15+
import kotlinx.coroutines.DelicateCoroutinesApi
16+
import kotlinx.coroutines.GlobalScope
17+
import kotlinx.coroutines.launch
18+
import kotlinx.datetime.Clock
19+
import xyz.flipchat.app.auth.AuthManager
20+
import xyz.flipchat.chat.RoomController
21+
import xyz.flipchat.services.user.UserManager
22+
import javax.inject.Inject
23+
import kotlin.coroutines.CoroutineContext
24+
import kotlin.coroutines.EmptyCoroutineContext
25+
26+
@AndroidEntryPoint
27+
class FcNotificationReceiver : BroadcastReceiver() {
28+
29+
@Inject
30+
lateinit var authManager: AuthManager
31+
32+
@Inject
33+
lateinit var userManager: UserManager
34+
35+
@Inject
36+
lateinit var roomController: RoomController
37+
38+
@Inject
39+
lateinit var notificationManager: NotificationManagerCompat
40+
41+
override fun onReceive(context: Context, intent: Intent) {
42+
val remoteInput = RemoteInput.getResultsFromIntent(intent)
43+
if (remoteInput != null) {
44+
val roomId = runCatching {
45+
Base58.decode(
46+
intent.getStringExtra(FcNotificationService.KEY_ROOM_ID).orEmpty()
47+
).toList()
48+
}.getOrNull()
49+
50+
val notificationId = intent.getIntExtra(FcNotificationService.KEY_NOTIFICATION_ID, -1).takeIf { it > 0 }
51+
if (notificationId != null) {
52+
val activeNotification = notificationManager.getActiveNotification(notificationId)
53+
if (activeNotification != null) {
54+
if (roomId != null) {
55+
val message =
56+
remoteInput.getCharSequence(FcNotificationService.KEY_TEXT_REPLY).toString()
57+
authenticateIfNeeded {
58+
goAsync {
59+
roomController.sendMessage(roomId, message)
60+
.onFailure {
61+
it.printStackTrace()
62+
}.onSuccess {
63+
println("Message sent via notification!")
64+
addReply(context, message, notificationId, activeNotification)
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}
71+
}
72+
}
73+
74+
@SuppressLint("MissingPermission")
75+
private fun addReply(
76+
context: Context,
77+
text: String,
78+
notificationId: Int,
79+
activeNotification: Notification
80+
) {
81+
val activeStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(activeNotification) ?: return
82+
83+
// Recover builder from the active notification.
84+
val recoveredBuilder = NotificationCompat.Builder(context, activeNotification)
85+
86+
val person = Person.Builder()
87+
.setName("You")
88+
.build()
89+
90+
val message = NotificationCompat.MessagingStyle.Message(
91+
text,
92+
Clock.System.now().toEpochMilliseconds(),
93+
person
94+
)
95+
96+
val newStyle = NotificationCompat.MessagingStyle(person)
97+
.setConversationTitle(activeStyle.conversationTitle)
98+
99+
activeStyle.messages.onEach { newStyle.addMessage(it) }
100+
101+
newStyle.addMessage(message)
102+
103+
// Set the new style to the recovered builder.
104+
recoveredBuilder.setStyle(newStyle)
105+
106+
// Update the active notification.
107+
NotificationManagerCompat.from(context).notify(notificationId, recoveredBuilder.build())
108+
}
109+
110+
private fun authenticateIfNeeded(block: () -> Unit) {
111+
if (userManager.userId == null) {
112+
authManager.init { block() }
113+
} else {
114+
block()
115+
}
116+
}
117+
}
118+
119+
fun BroadcastReceiver.goAsync(
120+
context: CoroutineContext = EmptyCoroutineContext,
121+
block: suspend CoroutineScope.() -> Unit
122+
) {
123+
val pendingResult = goAsync()
124+
@OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
125+
GlobalScope.launch(context) {
126+
try {
127+
block()
128+
} finally {
129+
pendingResult.finish()
130+
}
131+
}
132+
}

flipchatApp/src/main/kotlin/xyz/flipchat/app/services/FcNotificationService.kt renamed to flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/FcNotificationService.kt

Lines changed: 13 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package xyz.flipchat.app.services
1+
package xyz.flipchat.app.notifications
22

33
import android.Manifest
44
import android.app.Notification
@@ -17,7 +17,6 @@ import androidx.core.app.NotificationCompat
1717
import androidx.core.app.NotificationManagerCompat
1818
import androidx.core.app.Person
1919
import androidx.core.app.RemoteInput
20-
import com.getcode.model.ID
2120
import com.getcode.ui.components.chat.utils.localizedText
2221
import com.getcode.util.resources.ResourceHelper
2322
import com.getcode.util.resources.ResourceType
@@ -38,14 +37,13 @@ import xyz.flipchat.app.R
3837
import xyz.flipchat.app.auth.AuthManager
3938
import xyz.flipchat.app.theme.FC_Primary
4039
import xyz.flipchat.controllers.ChatsController
41-
import xyz.flipchat.controllers.CodeController
4240
import xyz.flipchat.controllers.PushController
4341
import xyz.flipchat.notifications.FcNotificationType
4442
import xyz.flipchat.notifications.parse
43+
import xyz.flipchat.services.user.AuthState
4544
import xyz.flipchat.services.user.UserManager
4645
import java.security.SecureRandom
4746
import javax.inject.Inject
48-
import kotlin.random.Random
4947

5048
@AndroidEntryPoint
5149
class FcNotificationService : FirebaseMessagingService(),
@@ -164,108 +162,22 @@ class FcNotificationService : FirebaseMessagingService(),
164162
return null
165163
}
166164

167-
val (id, notification) = when (type) {
168-
is FcNotificationType.ChatMessage -> buildChatNotification(type, title, content)
169-
FcNotificationType.Unknown -> buildMiscNotification(type, title, content)
170-
}
171-
172-
return id to notification.build()
173-
}
174-
175-
private fun buildChatNotification(
176-
type: FcNotificationType.ChatMessage,
177-
title: String,
178-
content: String
179-
): Pair<Int, NotificationCompat.Builder> {
180-
val sender = content.substringBefore(":")
181-
val messageBody = content.substringAfter(":")
182-
val person = Person.Builder()
183-
.setName(sender)
184-
.build()
185-
186-
val message = NotificationCompat.MessagingStyle.Message(
187-
messageBody,
188-
Clock.System.now().toEpochMilliseconds(),
189-
person
190-
)
191-
192-
val notificationId = type.id?.base58.hashCode()
193-
194-
val style = notificationManager.getActiveNotification(notificationId)?.let {
195-
NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(it)
196-
} ?: NotificationCompat.MessagingStyle(person)
197-
.setConversationTitle(title)
198-
.setGroupConversation(true)
199-
200-
val updatedStyle = style.addMessage(message)
201-
202-
val replyAction = if (type.id != null) {
203-
// build direct reply action
204-
val replyLabel: String = resources.getString(R.string.action_reply)
205-
val remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run {
206-
setLabel(replyLabel)
207-
build()
208-
}
209-
210-
val resultIntent = Intent(applicationContext, FcNotificationReceiver::class.java).apply {
211-
putExtra(KEY_ROOM_ID, type.id!!.base58)
212-
putExtra(KEY_NOTIFICATION_ID, notificationId)
213-
}
214-
215-
val replyPendingIntent: PendingIntent =
216-
PendingIntent.getBroadcast(
165+
with(notificationManager) {
166+
val (id, notification) = when (type) {
167+
is FcNotificationType.ChatMessage -> buildChatNotification(
217168
applicationContext,
218-
type.id.hashCode(),
219-
resultIntent,
220-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
169+
resources,
170+
type,
171+
title,
172+
content,
173+
userManager.authState is AuthState.LoggedIn
221174
)
222175

223-
NotificationCompat.Action.Builder(
224-
R.drawable.ic_reply,
225-
getString(R.string.action_reply),
226-
replyPendingIntent
227-
).addRemoteInput(remoteInput).build()
228-
} else {
229-
null
230-
}
231-
232-
val notificationBuilder: NotificationCompat.Builder =
233-
NotificationCompat.Builder(this, type.name)
234-
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
235-
.setStyle(updatedStyle)
236-
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
237-
.setSmallIcon(R.drawable.ic_flipchat_notification)
238-
.setColor(FC_Primary.toArgb())
239-
.setAutoCancel(true)
240-
.setContentIntent(buildContentIntent(type))
176+
FcNotificationType.Unknown -> buildMiscNotification(applicationContext, type, title, content)
177+
}
241178

242-
if (replyAction != null) {
243-
notificationBuilder.addAction(replyAction)
179+
return id to notification.build()
244180
}
245-
246-
return notificationId to notificationBuilder
247-
}
248-
249-
private fun buildMiscNotification(
250-
type: FcNotificationType,
251-
title: String,
252-
content: String
253-
): Pair<Int, NotificationCompat.Builder> {
254-
val notificationBuilder: NotificationCompat.Builder =
255-
NotificationCompat.Builder(this, type.name)
256-
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
257-
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
258-
.setSmallIcon(R.drawable.ic_flipchat_notification)
259-
.setColor(FC_Primary.toArgb())
260-
.setAutoCancel(true)
261-
.setContentTitle(title)
262-
.setContentText(content)
263-
.setContentIntent(buildContentIntent(type))
264-
265-
val random = SecureRandom()
266-
val notificationId = random.nextInt(256)
267-
268-
return notificationId to notificationBuilder
269181
}
270182

271183
private fun notify(
@@ -300,35 +212,6 @@ class FcNotificationService : FirebaseMessagingService(),
300212
}
301213
}
302214

303-
private fun NotificationManagerCompat.getActiveNotification(notificationId: Int): Notification? {
304-
val barNotifications = activeNotifications
305-
for (notification in barNotifications) {
306-
if (notification.id == notificationId) {
307-
return notification.notification
308-
}
309-
}
310-
return null
311-
}
312-
313-
private fun Context.buildContentIntent(type: FcNotificationType): PendingIntent {
314-
val launchIntent = when (type) {
315-
is FcNotificationType.ChatMessage -> Intent(Intent.ACTION_VIEW).apply {
316-
data = Uri.parse("https://app.flipchat.xyz/room?r=${type.id?.base58}")
317-
}
318-
319-
FcNotificationType.Unknown -> Intent(this, MainActivity::class.java).apply {
320-
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
321-
}
322-
}
323-
324-
return PendingIntent.getActivity(
325-
this,
326-
type.ordinal,
327-
launchIntent,
328-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
329-
)
330-
}
331-
332215
private fun String.localizedStringByKey(resources: ResourceHelper): String? {
333216
val name = this.replace(".", "_")
334217
val resId = resources.getIdentifier(

0 commit comments

Comments
 (0)