feat: notification tweaks features etc i really want to just ship it
This commit is contained in:
parent
f93c22ee14
commit
4f12e86fe8
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue