feat: notification tweaks features etc i really want to just ship it

This commit is contained in:
infi 2026-05-25 01:12:51 +02:00
parent f93c22ee14
commit 4f12e86fe8
6 changed files with 169 additions and 9 deletions

View File

@ -70,6 +70,10 @@
android:name=".c2dm.ReplyReceiver"
android:exported="false" />
<receiver
android:name=".c2dm.MarkAsReadReceiver"
android:exported="false" />
<activity
android:name=".activities.MainActivity"
android:exported="true"

View File

@ -271,6 +271,9 @@ class MainActivityViewModel(
viewModelScope.launch {
kvStorage.remove("sessionToken")
kvStorage.remove("sessionId")
kvStorage.remove("selfId")
kvStorage.remove("selfName")
kvStorage.remove("selfAvatarUrl")
startWithDestination("login/greeting")
}
}

View File

@ -3,6 +3,12 @@ package chat.stoat.c2dm
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
@ -12,6 +18,7 @@ import androidx.core.app.RemoteInput
import androidx.core.content.LocusIdCompat
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import chat.stoat.BuildConfig
import chat.stoat.R
@ -20,8 +27,9 @@ 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.schemas.ChannelType
import chat.stoat.core.model.data.STOAT_FILES
import chat.stoat.persistence.Database
import chat.stoat.persistence.KVStorage
import chat.stoat.persistence.SqlStorage
import com.bumptech.glide.Glide
import com.google.firebase.messaging.FirebaseMessagingService
@ -29,6 +37,40 @@ import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import logcat.logcat
import kotlin.math.abs
private val LETTER_ICON_COLORS = intArrayOf(
0xFF1565C0.toInt(),
0xFF2E7D32.toInt(),
0xFF6A1B9A.toInt(),
0xFFC62828.toInt(),
0xFF00838F.toInt(),
0xFFE65100.toInt(),
0xFF4527A0.toInt(),
0xFF283593.toInt(),
)
private fun generateLetterBitmap(name: String, sizePx: Int = 256): Bitmap {
val letter = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?"
val color = LETTER_ICON_COLORS[abs(name.hashCode()) % LETTER_ICON_COLORS.size]
val bitmap = createBitmap(sizePx, sizePx)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = color
canvas.drawCircle(sizePx / 2f, sizePx / 2f, sizePx / 2f, paint)
paint.color = Color.WHITE
paint.textSize = sizePx * 0.45f
paint.textAlign = Paint.Align.CENTER
paint.typeface = Typeface.DEFAULT_BOLD
val bounds = Rect()
paint.getTextBounds(letter, 0, 1, bounds)
canvas.drawText(letter, sizePx / 2f, sizePx / 2f + bounds.height() / 2f - bounds.bottom, paint)
return bitmap
}
object NotificationID {
const val NEW_MESSAGE = 0
@ -112,17 +154,57 @@ class HandlerService : FirebaseMessagingService() {
} ?: authorName
}
val bitmap = Glide.with(this)
fun loadBitmap(url: String) = Glide.with(this)
.asBitmap()
.load(image)
.load(url)
.circleCrop()
.submit()
.get()
val kv = KVStorage(this)
val selfId = runBlocking { kv.get("selfId") }.orEmpty()
val selfName = runBlocking { kv.get("selfName") }.orEmpty()
val selfAvatarUrl = runBlocking { kv.get("selfAvatarUrl") }
val selfBitmap: Bitmap = if (!selfAvatarUrl.isNullOrEmpty()) {
runCatching { loadBitmap(selfAvatarUrl) }.getOrNull()
?: generateLetterBitmap(selfName.ifEmpty { "?" })
} else {
generateLetterBitmap(selfName.ifEmpty { "?" })
}
val self = Person.Builder()
.setBot(false)
.setKey(selfId.ifEmpty { "self" })
.setIcon(IconCompat.createWithBitmap(selfBitmap))
.setName(selfName.ifEmpty { "Me" })
.build()
val dbChannel = db.channelQueries.findById(channelId).executeAsOneOrNull()
val authorBitmap = loadBitmap(image)
val conversationBitmap: Bitmap = when (dbChannel?.channelType) {
"TextChannel", "VoiceChannel" -> {
val server =
dbChannel.server?.let { db.serverQueries.findById(it).executeAsOneOrNull() }
val iconUrl = server?.iconId?.let { "$STOAT_FILES/icons/$it" }
iconUrl?.let { runCatching { loadBitmap(it) }.getOrNull() }
?: generateLetterBitmap(server?.name ?: channelName)
}
"Group" -> {
val iconUrl = dbChannel.iconId?.let { "$STOAT_FILES/icons/$it" }
iconUrl?.let { runCatching { loadBitmap(it) }.getOrNull() }
?: generateLetterBitmap(dbChannel.name ?: channelName)
}
else -> authorBitmap
}
val author = Person.Builder()
.setBot(false)
.setKey(authorId)
.setIcon(IconCompat.createWithBitmap(bitmap))
.setIcon(IconCompat.createWithBitmap(authorBitmap))
.setName(authorName)
.build()
@ -137,7 +219,7 @@ class HandlerService : FirebaseMessagingService() {
val shortcut = ShortcutInfoCompat.Builder(this, shortcutId)
.setShortLabel(channelName)
.setLongLabel(channelName)
.setIcon(IconCompat.createWithBitmap(bitmap))
.setIcon(IconCompat.createWithBitmap(conversationBitmap))
.setIntent(conversationIntent)
.setLongLived(true)
.setPerson(author)
@ -154,7 +236,7 @@ class HandlerService : FirebaseMessagingService() {
putExtra("channelId", channelId)
}
val action: NotificationCompat.Action =
val replyAction: NotificationCompat.Action =
NotificationCompat.Action.Builder(
R.drawable.ic_reply_24dp,
getString(R.string.message_context_sheet_actions_reply),
@ -168,6 +250,24 @@ class HandlerService : FirebaseMessagingService() {
.addRemoteInput(remoteInput)
.build()
val markAsReadIntent = Intent(this, MarkAsReadReceiver::class.java).apply {
putExtra("channelId", channelId)
putExtra("messageId", messageId)
}
val markAsReadAction: NotificationCompat.Action =
NotificationCompat.Action.Builder(
R.drawable.ic_mark_chat_read_24dp,
getString(R.string.channel_context_sheet_actions_mark_read),
PendingIntent.getBroadcast(
this,
channelId.hashCode() xor 1,
markAsReadIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.build()
val contentIntent = PendingIntent.getActivity(
this,
channelId.hashCode(),
@ -182,11 +282,13 @@ class HandlerService : FirebaseMessagingService() {
.setContentIntent(contentIntent)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setStyle(
NotificationCompat.MessagingStyle(author)
NotificationCompat.MessagingStyle(self)
.setGroupConversation(dbChannel?.channelType != "DirectMessage")
.setConversationTitle(channelName)
.addMessage(body, messageTimestamp, author)
)
.addAction(action)
.addAction(replyAction)
.addAction(markAsReadAction)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
@ -204,7 +306,7 @@ class HandlerService : FirebaseMessagingService() {
val bubbleMetadata = NotificationCompat.BubbleMetadata.Builder(
bubbleIntent,
IconCompat.createWithBitmap(bitmap)
IconCompat.createWithBitmap(conversationBitmap)
)
.setDesiredHeight(600)
.setAutoExpandBubble(false)

View File

@ -0,0 +1,39 @@
package chat.stoat.c2dm
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationManagerCompat
import chat.stoat.api.StoatAPI
import chat.stoat.api.routes.channel.ackChannel
import chat.stoat.persistence.KVStorage
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import logcat.asLog
import logcat.logcat
class MarkAsReadReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val channelId = intent.getStringExtra("channelId") ?: run {
logcat(LogPriority.ERROR) { "No channel ID, aborting" }
return
}
val messageId = intent.getStringExtra("messageId") ?: run {
logcat(LogPriority.ERROR) { "No message ID, aborting" }
return
}
runBlocking {
val token = KVStorage(context).get("sessionToken") ?: run {
logcat(LogPriority.ERROR) { "No session token, aborting" }
return@runBlocking
}
StoatAPI.setSessionHeader(token)
runCatching { ackChannel(channelId, messageId) }
.onFailure { logcat(LogPriority.ERROR) { "Ack failed: ${it.asLog()}" } }
}
NotificationManagerCompat.from(context).cancel(channelId, NotificationID.NEW_MESSAGE)
}
}

View File

@ -73,6 +73,9 @@ import chat.stoat.api.realtime.DisconnectionState
import chat.stoat.api.realtime.RealtimeSocket
import chat.stoat.api.routes.microservices.gazette.getLatestChangelog
import chat.stoat.api.routes.push.subscribePush
import chat.stoat.api.routes.user.fetchSelf
import chat.stoat.core.model.data.STOAT_FILES
import chat.stoat.core.model.schemas.User
import chat.stoat.api.settings.SyncedSettings
import chat.stoat.callbacks.Action
import chat.stoat.callbacks.ActionChannel
@ -162,6 +165,12 @@ class ChatRouterViewModel(
init {
viewModelScope.launch {
runCatching { fetchSelf() }.getOrNull()?.let { user ->
kvStorage.set("selfId", user.id ?: "")
kvStorage.set("selfName", User.resolveDefaultName(user))
kvStorage.set("selfAvatarUrl", user.avatar?.id?.let { "$STOAT_FILES/avatars/$it" } ?: "")
}
val pendingChannel = NotificationDeepLink.pendingChannelId.value
if (pendingChannel != null) {
NotificationDeepLink.pendingChannelId.value = null

View File

@ -48,6 +48,9 @@ class SettingsScreenViewModel(
fun logout() {
runBlocking {
kvStorage.remove("sessionToken")
kvStorage.remove("selfId")
kvStorage.remove("selfName")
kvStorage.remove("selfAvatarUrl")
LoadedSettings.reset()
StoatAPI.logout()
}