Skip to content

Commit 3b397ee

Browse files
committed
Update to handle data payloads
1 parent bf849d8 commit 3b397ee

File tree

19 files changed

+312
-54
lines changed

19 files changed

+312
-54
lines changed

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composeApp/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ kotlin {
8484
implementation(libs.androidx.startup.runtime)
8585
// Pinch to zoom
8686
implementation(libs.lib.zoomable)
87+
implementation(project.dependencies.platform(libs.firebase.bom))
88+
implementation(libs.firebase.messaging)
8789
}
8890
commonMain.dependencies {
8991
implementation(kotlin("reflect"))

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
</intent-filter>
2424
</activity>
2525

26+
<service
27+
android:name=".MessagingService"
28+
android:exported="false">
29+
<intent-filter>
30+
<action android:name="com.google.firebase.MESSAGING_EVENT" />
31+
</intent-filter>
32+
</service>
33+
2634
<meta-data
2735
android:name="com.google.firebase.messaging.default_notification_icon"
2836
android:resource="@drawable/ic_stat_chat_icon" />

composeApp/src/androidMain/kotlin/com/hypergonial/chat/MainActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
1414
import androidx.compose.runtime.CompositionLocalProvider
1515
import androidx.compose.ui.graphics.Color
1616
import androidx.compose.ui.platform.LocalContext
17+
import co.touchlab.kermit.Logger
1718
import com.arkivanov.decompose.defaultComponentContext
1819
import com.hypergonial.chat.model.AndroidSettings
1920
import com.hypergonial.chat.model.settings
@@ -41,6 +42,9 @@ class MainActivity : ComponentActivity() {
4142
showPushNotification = false,
4243
)
4344
)
45+
NotifierManager.setLogger {
46+
Logger.withTag("NotifierManager").i(it)
47+
}
4448

4549
val permissionUtil by permissionUtil()
4650
permissionUtil.askNotificationPermission()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.hypergonial.chat
2+
3+
import co.touchlab.kermit.Logger
4+
import com.google.firebase.messaging.FirebaseMessagingService
5+
import com.google.firebase.messaging.RemoteMessage
6+
import com.hypergonial.chat.model.AndroidSettings
7+
import com.hypergonial.chat.model.payloads.toSnowflake
8+
import com.hypergonial.chat.model.settings
9+
import com.hypergonial.chat.view.notificationProvider
10+
import com.mmk.kmpnotifier.notification.NotifierManager
11+
import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration
12+
13+
class MessagingService : FirebaseMessagingService() {
14+
15+
override fun onNewToken(token: String) {
16+
super.onNewToken(token)
17+
}
18+
19+
override fun onMessageReceived(message: RemoteMessage) {
20+
super.onMessageReceived(message)
21+
22+
if (settings !is AndroidSettings) {
23+
return
24+
}
25+
26+
settings.initialize(getSharedPreferences("settings", MODE_PRIVATE))
27+
28+
// Send local notification
29+
NotifierManager.initialize(
30+
configuration =
31+
NotificationPlatformConfiguration.Android(
32+
notificationIconResId = R.drawable.ic_stat_chat_icon,
33+
showPushNotification = false,
34+
)
35+
)
36+
NotifierManager.setLogger {
37+
Logger.withTag("NotifierManager").i(it)
38+
}
39+
40+
notificationProvider.sendNotification {
41+
channelId = message.data["channel_id"]?.toSnowflake()
42+
title = message.data["title"] ?: "You got mail!"
43+
body = message.data["body"] ?: "No content provided."
44+
payloadData = message.data
45+
}
46+
}
47+
}
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
11
package com.hypergonial.chat.view
22

3-
import com.mmk.kmpnotifier.notification.NotifierBuilder
4-
import com.mmk.kmpnotifier.notification.NotifierManager
5-
6-
actual fun sendNotification(builder: NotifierBuilder.() -> Unit) {
7-
NotifierManager.getLocalNotifier().notify(builder)
8-
}
3+
actual val notificationProvider: NotificationProvider = DefaultNotificationProvider

composeApp/src/commonMain/kotlin/com/hypergonial/chat/model/AppSettings.kt

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import com.hypergonial.chat.SettingsExt.setSerializable
55
import com.hypergonial.chat.model.payloads.Snowflake
66
import com.hypergonial.chat.model.payloads.toSnowflake
77
import com.russhwolf.settings.Settings
8+
import kotlinx.atomicfu.locks.SynchronizedObject
9+
import kotlinx.atomicfu.locks.synchronized
810
import kotlinx.datetime.Instant
911

1012
/** The base class for implementing a persistent settings store for the application */
11-
abstract class AppSettings {
13+
abstract class AppSettings : SynchronizedObject() {
1214
/**
1315
* The user preferences store
1416
*
@@ -24,8 +26,12 @@ abstract class AppSettings {
2426
*/
2527
protected abstract val secrets: Settings?
2628

29+
private var _cachedDevSettings: DevSettings? = null
30+
2731
/** The cached developer settings for faster access */
28-
private var cachedDevSettings: DevSettings? = null
32+
private var cachedDevSettings: DevSettings?
33+
get() = synchronized(this) { _cachedDevSettings }
34+
set(value) = synchronized(this) { _cachedDevSettings = value }
2935

3036
/**
3137
* Get a secret from the settings, uses the secrets store if one is available on the platform.
@@ -101,6 +107,54 @@ abstract class AppSettings {
101107
/** Remove the current user's FCM token */
102108
fun clearFCMSettings() = userPreferences.remove("FCM_SETTINGS")
103109

110+
/** Get all currently active notifications */
111+
private fun getNotifications(): HashMap<Snowflake, HashSet<Int>> {
112+
synchronized(this) {
113+
val notifs = userPreferences.getSerializable<HashMap<Snowflake, HashSet<Int>>>("NOTIFICATIONS")
114+
return notifs ?: HashMap()
115+
}
116+
}
117+
118+
/** Set all currently active notifications */
119+
private fun setNotifications(notifs: HashMap<Snowflake, HashSet<Int>>) {
120+
synchronized(this) { userPreferences.setSerializable("NOTIFICATIONS", notifs) }
121+
}
122+
123+
/** Add a new notification to the list of active notifications. */
124+
fun pushNotification(channelId: Snowflake, notificationId: Int) {
125+
synchronized(this) {
126+
val notifs = getNotifications()
127+
notifs.getOrPut(channelId) { HashSet() }.add(notificationId)
128+
setNotifications(notifs)
129+
}
130+
}
131+
132+
/** Remove a notification from the list of active notifications. */
133+
fun popNotification(channelId: Snowflake, notificationId: Int) {
134+
synchronized(this) {
135+
val notifs = getNotifications()
136+
notifs[channelId]?.remove(notificationId)
137+
setNotifications(notifs)
138+
}
139+
}
140+
141+
/** Get all notifications for a specific channel */
142+
fun getNotificationsIn(channelId: Snowflake): Set<Int>? {
143+
synchronized(this) {
144+
val notifs = getNotifications()
145+
return notifs[channelId]
146+
}
147+
}
148+
149+
/** Clear all notifications for a specific channel */
150+
fun clearNotificationsIn(channelId: Snowflake) {
151+
synchronized(this) {
152+
val notifs = getNotifications()
153+
notifs.remove(channelId)
154+
setNotifications(notifs)
155+
}
156+
}
157+
104158
/** Get the last user's ID we logged in as */
105159
fun getLastLoggedInAs(): Snowflake? {
106160
val value = userPreferences.getStringOrNull("LAST_LOGGED_IN_ID")

composeApp/src/commonMain/kotlin/com/hypergonial/chat/model/ChatClient.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ class ChatClient(scope: CoroutineScope, override val maxReconnectAttempts: Int =
359359
override fun pause() {
360360
_isSuspended = true
361361
closeGateway()
362-
eventManager.dispatch(LifecyclePausedEvent())
362+
eventManager.dispatch(ClientPausedEvent())
363363
cache.clearMessageCache()
364364
}
365365

@@ -383,7 +383,7 @@ class ChatClient(scope: CoroutineScope, override val maxReconnectAttempts: Int =
383383
}
384384
}
385385

386-
eventManager.dispatch(LifecycleResumedEvent())
386+
eventManager.dispatch(ClientResumedEvent())
387387
}
388388

389389
override fun isLoggedIn(): Boolean {

composeApp/src/commonMain/kotlin/com/hypergonial/chat/model/Event.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,19 @@ class NotificationClickedEvent(val guildId: Snowflake, val channelId: Snowflake)
114114
class FocusAssetEvent(val url: String) : InternalEvent()
115115

116116
/** Internal event dispatched when the client was resumed */
117-
class LifecycleResumedEvent : InternalEvent()
117+
class ClientResumedEvent : InternalEvent()
118118

119119
/** Internal event dispatched when the client was paused */
120+
class ClientPausedEvent : InternalEvent()
121+
122+
/** Internal event dispatched when the app's global lifecycle is stopped */
123+
class LifecycleStoppedEvent : InternalEvent()
124+
125+
/** Internal event dispatched when the app's global lifecycle is paused */
120126
class LifecyclePausedEvent : InternalEvent()
127+
128+
/** Internal event dispatched when the app's global lifecycle is resumed */
129+
class LifecycleResumedEvent : InternalEvent()
130+
131+
/** Internal event dispatched when the app's global lifecycle is destroyed */
132+
class LifecycleDestroyedEvent : InternalEvent()
Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,68 @@
11
package com.hypergonial.chat.view
22

3+
import com.hypergonial.chat.model.payloads.Snowflake
4+
import com.hypergonial.chat.model.settings
5+
import com.mmk.kmpnotifier.notification.NotificationImage
36
import com.mmk.kmpnotifier.notification.NotifierBuilder
7+
import com.mmk.kmpnotifier.notification.NotifierManager
8+
import kotlin.random.Random
49

5-
expect fun sendNotification(builder: NotifierBuilder.() -> Unit)
10+
interface NotificationProvider {
11+
fun sendNotification(builder: NotificationBuilder.() -> Unit)
12+
fun dismissNotification(channelId: Snowflake, id: Int)
13+
fun dismissAllForChannel(channelId: Snowflake) {
14+
val ids = settings.getNotificationsIn(channelId)
15+
ids?.forEach { id ->
16+
dismissNotification(channelId, id)
17+
}
18+
}
19+
}
20+
21+
class NotificationBuilder {
22+
var channelId: Snowflake? = null
23+
var id: Int = Random.nextInt(0, Int.MAX_VALUE)
24+
var title: String = ""
25+
var body: String = ""
26+
27+
var payloadData: Map<String, String> = emptyMap()
28+
29+
var image: NotificationImage? = null
30+
31+
companion object {
32+
fun toKMPNotify(
33+
builder: NotificationBuilder
34+
): NotifierBuilder.() -> Unit {
35+
return {
36+
this.id = builder.id
37+
this.title = builder.title
38+
this.body = builder.body
39+
this.payloadData = builder.payloadData
40+
this.image = builder.image
41+
}
42+
}
43+
}
44+
}
45+
46+
/**
47+
* Default notification provider that uses the KMP local notifier.
48+
*
49+
* This is used to send notifications to the user.
50+
*/
51+
object DefaultNotificationProvider : NotificationProvider {
52+
override fun sendNotification(builder: NotificationBuilder.() -> Unit) {
53+
val data = NotificationBuilder().apply(builder)
54+
val channelId = data.channelId
55+
56+
require(channelId != null) { "Channel ID must be set" }
57+
58+
NotifierManager.getLocalNotifier().notify(NotificationBuilder.toKMPNotify(data))
59+
settings.pushNotification(channelId, data.id)
60+
}
61+
62+
override fun dismissNotification(channelId: Snowflake, id: Int) {
63+
NotifierManager.getLocalNotifier().remove(id)
64+
settings.popNotification(channelId, id)
65+
}
66+
}
67+
68+
expect val notificationProvider: NotificationProvider

0 commit comments

Comments
 (0)