feat: push notifications with everything around
This commit is contained in:
parent
1e2ef35df0
commit
2b84cbb342
|
|
@ -66,6 +66,10 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".c2dm.ReplyReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ import chat.stoat.R
|
|||
import chat.stoat.StoatApplication
|
||||
import chat.stoat.api.HitRateLimitException
|
||||
import chat.stoat.api.StoatAPI
|
||||
import chat.stoat.c2dm.NotificationDeepLink
|
||||
import chat.stoat.api.StoatHttp
|
||||
import chat.stoat.api.api
|
||||
import chat.stoat.api.routes.microservices.geo.queryGeo
|
||||
|
|
@ -354,6 +355,8 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
StoatAPI.hydrateFromPersistentCache()
|
||||
|
||||
intent.getStringExtra("channelId")?.let { NotificationDeepLink.pendingChannelId.value = it }
|
||||
|
||||
setContent {
|
||||
val windowSizeClass = calculateWindowSizeClass(this)
|
||||
AppEntrypoint(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import android.app.PendingIntent
|
|||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
|
@ -17,22 +16,19 @@ import androidx.core.graphics.drawable.IconCompat
|
|||
import chat.stoat.BuildConfig
|
||||
import chat.stoat.R
|
||||
import chat.stoat.activities.MainActivity
|
||||
import chat.stoat.api.StoatJson
|
||||
import chat.stoat.api.internals.ULID
|
||||
import chat.stoat.api.routes.channel.fetchSingleChannel
|
||||
import chat.stoat.api.routes.push.subscribePush
|
||||
import chat.stoat.c2dm.ChannelRegistrator.Companion.CHANNEL_ID_GROUP_CONVERSATIONS_MESSAGES
|
||||
import chat.stoat.core.model.data.STOAT_BASE
|
||||
import chat.stoat.core.model.schemas.Message
|
||||
import chat.stoat.core.model.schemas.User
|
||||
import chat.stoat.core.model.schemas.ChannelType
|
||||
import chat.stoat.persistence.Database
|
||||
import chat.stoat.persistence.SqlStorage
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import logcat.LogPriority
|
||||
import logcat.logcat
|
||||
|
||||
object NotificationID {
|
||||
const val NEW_MESSAGE = 0
|
||||
|
|
@ -47,96 +43,82 @@ class HandlerService : FirebaseMessagingService() {
|
|||
}
|
||||
|
||||
override fun onMessageReceived(fcmMessage: RemoteMessage) {
|
||||
/// TEMPORARY CODE, SCHEMA TO BE REPLACED
|
||||
val payloadString = fcmMessage.data["payload"]
|
||||
if (payloadString == null) {
|
||||
Log.e("HandlerService", "No payload in message, abort")
|
||||
val data = fcmMessage.data
|
||||
|
||||
val type = data["type"]
|
||||
if (type != "push.message") {
|
||||
logcat(LogPriority.ERROR) { "Unknown message type: $type, abort" }
|
||||
return
|
||||
}
|
||||
|
||||
Log.d("HandlerService", payloadString)
|
||||
|
||||
val payload = StoatJson.parseToJsonElement(payloadString).jsonObject
|
||||
val keys = payload.keys.toList().toString()
|
||||
Log.d("HandlerService", "following keys: $keys")
|
||||
|
||||
var authorIcon = payload["icon"]?.jsonPrimitive?.contentOrNull
|
||||
val message = payload["message"]?.jsonObject?.let {
|
||||
StoatJson.decodeFromJsonElement(
|
||||
Message.serializer(),
|
||||
it
|
||||
)
|
||||
} ?: run {
|
||||
Log.e("HandlerService", "No message in payload, abort")
|
||||
val authorId = data["author"] ?: run {
|
||||
logcat(LogPriority.ERROR) { "No author in message, abort" }
|
||||
return
|
||||
}
|
||||
|
||||
val user = payload["message"]?.jsonObject?.get("user")?.jsonObject?.let {
|
||||
StoatJson.decodeFromJsonElement(
|
||||
User.serializer(),
|
||||
it
|
||||
)
|
||||
} ?: run {
|
||||
Log.e("HandlerService", "No message->user in payload, abort")
|
||||
val body = data["body"] ?: run {
|
||||
logcat(LogPriority.ERROR) { "No body in message, abort" }
|
||||
return
|
||||
}
|
||||
|
||||
if (authorIcon == null) {
|
||||
authorIcon =
|
||||
"$STOAT_BASE/users/${message.author?.ifBlank { "0".repeat(26) }}/default_avatar"
|
||||
val image = data["image"] ?: run {
|
||||
logcat(LogPriority.WARN) { "No image in message, abort" }
|
||||
return
|
||||
}
|
||||
|
||||
val authorName = data["author_name"] ?: run {
|
||||
logcat(LogPriority.ERROR) { "No author name in message, abort" }
|
||||
return
|
||||
}
|
||||
|
||||
val channelId = data["channel"] ?: run {
|
||||
logcat(LogPriority.ERROR) { "No channel in message, abort" }
|
||||
return
|
||||
}
|
||||
|
||||
val messageId = data["message"] ?: run {
|
||||
logcat(LogPriority.ERROR) { "No message ID in message, abort" }
|
||||
return
|
||||
}
|
||||
|
||||
val messageTimestamp = ULID.asTimestamp(messageId)
|
||||
|
||||
val db = Database(SqlStorage.driver)
|
||||
val channelName = message.channel?.let {
|
||||
db.channelQueries.findById(it).executeAsOneOrNull()
|
||||
}?.let {
|
||||
val channelName = db.channelQueries.findById(channelId).executeAsOneOrNull()?.let {
|
||||
when (it.channelType) {
|
||||
"DirectMessage" -> {
|
||||
user.displayName ?: user.username
|
||||
}
|
||||
|
||||
"TextChannel" -> {
|
||||
"#${it.name}"
|
||||
}
|
||||
|
||||
else -> {
|
||||
it.name ?: getString(R.string.unknown)
|
||||
}
|
||||
"DirectMessage" -> authorName
|
||||
"TextChannel" -> "#${it.name}"
|
||||
else -> it.name ?: authorName
|
||||
}
|
||||
} ?: getString(
|
||||
R.string.unknown
|
||||
)
|
||||
|
||||
val messageTimestamp = message.id?.let { ULID.asTimestamp(it) } ?: run {
|
||||
Log.e("HandlerService", "No message id in message, abort")
|
||||
return
|
||||
} ?: runBlocking {
|
||||
runCatching { fetchSingleChannel(channelId) }.getOrNull()?.let {
|
||||
when (it.channelType) {
|
||||
ChannelType.DirectMessage -> authorName
|
||||
ChannelType.TextChannel -> "#${it.name}"
|
||||
else -> it.name ?: authorName
|
||||
}
|
||||
} ?: authorName
|
||||
}
|
||||
|
||||
val bitmap = Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(authorIcon)
|
||||
.load(image)
|
||||
.circleCrop()
|
||||
.submit()
|
||||
.get()
|
||||
|
||||
val author =
|
||||
Person.Builder()
|
||||
.setBot(user.bot != null)
|
||||
.setKey(message.author)
|
||||
.setIcon(IconCompat.createWithBitmap(bitmap))
|
||||
.setName(user.displayName ?: user.username)
|
||||
.build()
|
||||
val author = Person.Builder()
|
||||
.setBot(false)
|
||||
.setKey(authorId)
|
||||
.setIcon(IconCompat.createWithBitmap(bitmap))
|
||||
.setName(authorName)
|
||||
.build()
|
||||
|
||||
if (message.channel == null) {
|
||||
Log.e("HandlerService", "No channel in message, abort")
|
||||
return
|
||||
}
|
||||
|
||||
val shortcutId = "${BuildConfig.APPLICATION_ID}.channel.${message.channel}"
|
||||
val shortcutId = "${BuildConfig.APPLICATION_ID}.channel.$channelId"
|
||||
|
||||
val conversationIntent = Intent(this, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
putExtra("channelId", message.channel)
|
||||
putExtra("channelId", channelId)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
|
|
@ -156,14 +138,18 @@ class HandlerService : FirebaseMessagingService() {
|
|||
build()
|
||||
}
|
||||
|
||||
val replyIntent = Intent(this, ReplyReceiver::class.java).apply {
|
||||
putExtra("channelId", channelId)
|
||||
}
|
||||
|
||||
val action: NotificationCompat.Action =
|
||||
NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_reply_24dp,
|
||||
getString(R.string.message_context_sheet_actions_reply),
|
||||
PendingIntent.getActivity(
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
conversationIntent,
|
||||
channelId.hashCode(),
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
|
|
@ -172,25 +158,21 @@ class HandlerService : FirebaseMessagingService() {
|
|||
|
||||
val contentIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
message.channel.hashCode(),
|
||||
channelId.hashCode(),
|
||||
conversationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID_GROUP_CONVERSATIONS_MESSAGES)
|
||||
.setSmallIcon(R.drawable.ic_chat_24dp)
|
||||
.setContentTitle(user.displayName ?: user.username)
|
||||
.setContentText(message.content)
|
||||
.setSmallIcon(R.drawable.ic_stoat_24dp)
|
||||
.setContentTitle(authorName)
|
||||
.setContentText(body)
|
||||
.setContentIntent(contentIntent)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setStyle(
|
||||
NotificationCompat.MessagingStyle(author)
|
||||
.setConversationTitle(channelName)
|
||||
.addMessage(
|
||||
message.content ?: getString(R.string.reply_message_empty_has_attachments),
|
||||
messageTimestamp,
|
||||
author
|
||||
)
|
||||
.addMessage(body, messageTimestamp, author)
|
||||
)
|
||||
.addAction(action)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
|
@ -203,7 +185,7 @@ class HandlerService : FirebaseMessagingService() {
|
|||
|
||||
val bubbleIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
message.channel.hashCode(),
|
||||
channelId.hashCode(),
|
||||
conversationIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
|
@ -228,8 +210,7 @@ class HandlerService : FirebaseMessagingService() {
|
|||
) {
|
||||
return
|
||||
}
|
||||
notify(message.channel, NotificationID.NEW_MESSAGE, builder.build())
|
||||
notify(channelId, NotificationID.NEW_MESSAGE, builder.build())
|
||||
}
|
||||
/// END TEMPORARY CODE
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package chat.stoat.c2dm
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
object NotificationDeepLink {
|
||||
val pendingChannelId = MutableStateFlow<String?>(null)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package chat.stoat.c2dm
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.RemoteInput
|
||||
import chat.stoat.api.StoatAPI
|
||||
import chat.stoat.api.routes.channel.sendMessage
|
||||
import chat.stoat.persistence.KVStorage
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import logcat.LogPriority
|
||||
import logcat.asLog
|
||||
import logcat.logcat
|
||||
|
||||
class ReplyReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val channelId = intent.getStringExtra("channelId") ?: run {
|
||||
logcat(LogPriority.ERROR) { "No channel ID, aborting" }
|
||||
return
|
||||
}
|
||||
|
||||
val content = RemoteInput.getResultsFromIntent(intent)
|
||||
?.getCharSequence("content")
|
||||
?.toString()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: run {
|
||||
logcat(LogPriority.ERROR) { "No content, aborting" }
|
||||
return
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
val token = KVStorage(context).get("sessionToken") ?: run {
|
||||
logcat(LogPriority.ERROR) { "No session token, aborting" }
|
||||
return@runBlocking
|
||||
}
|
||||
StoatAPI.setSessionHeader(token)
|
||||
runCatching { sendMessage(channelId, content) }
|
||||
.onFailure { logcat(LogPriority.ERROR) { "Send failed: ${it.asLog()}" } }
|
||||
}
|
||||
|
||||
NotificationManagerCompat.from(context).cancel(channelId, NotificationID.NEW_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
|
@ -80,6 +80,7 @@ import chat.stoat.composables.chat.DisconnectedNotice
|
|||
import chat.stoat.composables.screens.chat.drawer.ChannelSideDrawer
|
||||
import chat.stoat.core.model.schemas.ReleaseNotesSettings
|
||||
import chat.stoat.dialogs.NotificationRationaleDialog
|
||||
import chat.stoat.c2dm.NotificationDeepLink
|
||||
import chat.stoat.internals.extensions.zero
|
||||
import chat.stoat.persistence.KVStorage
|
||||
import chat.stoat.screens.chat.dialogs.safety.ReportMessageDialog
|
||||
|
|
@ -161,8 +162,14 @@ class ChatRouterViewModel(
|
|||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val current = kvStorage.get("currentDestination")
|
||||
setSaveDestination(ChatRouterDestination.fromString(current ?: ""))
|
||||
val pendingChannel = NotificationDeepLink.pendingChannelId.value
|
||||
if (pendingChannel != null) {
|
||||
NotificationDeepLink.pendingChannelId.value = null
|
||||
setSaveDestination(ChatRouterDestination.Channel(pendingChannel))
|
||||
} else {
|
||||
val current = kvStorage.get("currentDestination")
|
||||
setSaveDestination(ChatRouterDestination.fromString(current ?: ""))
|
||||
}
|
||||
|
||||
val seenEarlyAccess = kvStorage.getBoolean("spark/earlyAccess/dismissed")
|
||||
val seenSwipeToReply = kvStorage.getBoolean("spark/swipeToReply/dismissed")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3.822,20.779C1.471,18.587 0,15.464 0,12C0,5.377 5.377,0 12,0C18.623,0 24,5.377 24,12C24,17.512 20.276,22.161 15.209,23.566C14.084,21.913 13.065,20.408 13.827,17.931C12.897,18.405 12.313,19.339 12.136,20.353C11.79,19.81 11.406,19.285 10.961,18.818C9.828,17.628 8.284,16.797 7.569,15.252C7.243,14.523 7.019,13.48 7.278,12.725C7.464,14.19 8.177,15.538 9.454,16.319C10.427,16.913 11.517,17.012 12.609,17.242C13.747,17.482 15.554,18.022 16.663,17.827C17.584,17.665 19.518,16.647 19.466,15.543C19.448,15.163 18.985,14.523 18.813,14.156C18.585,13.668 18.72,13.563 18.746,13.073C18.82,11.688 18.454,11.003 17.781,9.862C16.965,8.478 16.069,7.642 14.517,7.08C13.1,6.567 10.918,6.858 10.901,6.881C11.188,7.195 11.451,7.563 11.488,7.998C10.907,7.461 10.345,6.889 9.694,6.434C8.255,5.426 7.353,5.521 6.781,7.274C6.483,8.187 6.257,9.428 6.534,10.362C6.701,10.928 7.049,11.388 7.446,11.813C6.67,11.707 6.2,11.033 6.029,10.303C4.107,14.461 3.679,16.472 3.822,20.779ZM18.082,16.797L18.081,16.797C17.783,16.806 17.508,16.647 17.282,16.466C17.156,16.358 16.969,16.219 16.943,16.058C16.892,15.737 17.147,15.507 17.3,15.412C17.43,15.331 17.589,15.285 17.739,15.251C18.081,15.172 18.435,15.099 18.78,15.165C18.883,15.185 18.985,15.219 19.07,15.281C19.154,15.342 19.22,15.435 19.232,15.54C19.24,15.617 19.219,15.694 19.193,15.768C19.134,15.936 19.052,16.096 18.949,16.243C18.847,16.389 18.726,16.523 18.579,16.624C18.433,16.725 18.26,16.792 18.082,16.797ZM12.982,12.083C14.162,11.923 14.306,13.751 13.167,13.827C12.029,13.904 11.901,12.229 12.982,12.083ZM9.006,8.454C9.115,8.714 9.339,8.887 9.533,9.078C9.093,9.211 8.648,9.084 8.262,8.862C8.285,9.271 8.587,9.631 8.813,9.918C8.44,10.003 8.11,9.875 7.854,9.606C7.85,9.985 7.98,10.316 8.166,10.637C7.438,10.999 7.124,10.238 7.086,9.642C7.046,8.987 7.311,7.334 7.781,6.858C8.307,6.325 9.123,6.946 9.568,7.303C9.961,7.619 10.34,8.001 10.696,8.356C10.228,8.665 9.532,8.652 9.006,8.454ZM18.153,13.226C17.896,12.779 17.817,12.649 17.668,12.325C17.566,12.104 17.514,11.842 17.883,11.834C18.553,11.82 18.785,12.881 18.264,13.251C18.251,13.26 18.235,13.265 18.22,13.265C18.192,13.265 18.167,13.25 18.153,13.226ZM17.713,8.956C17.903,6.344 17.881,4.654 15.459,7.025C16.373,7.45 17.142,8.129 17.713,8.956Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
Loading…
Reference in New Issue