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:name=".c2dm.ReplyReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".c2dm.MarkAsReadReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,9 @@ class MainActivityViewModel(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
kvStorage.remove("sessionToken")
|
kvStorage.remove("sessionToken")
|
||||||
kvStorage.remove("sessionId")
|
kvStorage.remove("sessionId")
|
||||||
|
kvStorage.remove("selfId")
|
||||||
|
kvStorage.remove("selfName")
|
||||||
|
kvStorage.remove("selfAvatarUrl")
|
||||||
startWithDestination("login/greeting")
|
startWithDestination("login/greeting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ package chat.stoat.c2dm
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
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 android.os.Build
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
|
@ -12,6 +18,7 @@ import androidx.core.app.RemoteInput
|
||||||
import androidx.core.content.LocusIdCompat
|
import androidx.core.content.LocusIdCompat
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import chat.stoat.BuildConfig
|
import chat.stoat.BuildConfig
|
||||||
import chat.stoat.R
|
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.channel.fetchSingleChannel
|
||||||
import chat.stoat.api.routes.push.subscribePush
|
import chat.stoat.api.routes.push.subscribePush
|
||||||
import chat.stoat.c2dm.ChannelRegistrator.Companion.CHANNEL_ID_GROUP_CONVERSATIONS_MESSAGES
|
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.Database
|
||||||
|
import chat.stoat.persistence.KVStorage
|
||||||
import chat.stoat.persistence.SqlStorage
|
import chat.stoat.persistence.SqlStorage
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
|
@ -29,6 +37,40 @@ import com.google.firebase.messaging.RemoteMessage
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.logcat
|
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 {
|
object NotificationID {
|
||||||
const val NEW_MESSAGE = 0
|
const val NEW_MESSAGE = 0
|
||||||
|
|
@ -112,17 +154,57 @@ class HandlerService : FirebaseMessagingService() {
|
||||||
} ?: authorName
|
} ?: authorName
|
||||||
}
|
}
|
||||||
|
|
||||||
val bitmap = Glide.with(this)
|
fun loadBitmap(url: String) = Glide.with(this)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(image)
|
.load(url)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.submit()
|
.submit()
|
||||||
.get()
|
.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()
|
val author = Person.Builder()
|
||||||
.setBot(false)
|
.setBot(false)
|
||||||
.setKey(authorId)
|
.setKey(authorId)
|
||||||
.setIcon(IconCompat.createWithBitmap(bitmap))
|
.setIcon(IconCompat.createWithBitmap(authorBitmap))
|
||||||
.setName(authorName)
|
.setName(authorName)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|
@ -137,7 +219,7 @@ class HandlerService : FirebaseMessagingService() {
|
||||||
val shortcut = ShortcutInfoCompat.Builder(this, shortcutId)
|
val shortcut = ShortcutInfoCompat.Builder(this, shortcutId)
|
||||||
.setShortLabel(channelName)
|
.setShortLabel(channelName)
|
||||||
.setLongLabel(channelName)
|
.setLongLabel(channelName)
|
||||||
.setIcon(IconCompat.createWithBitmap(bitmap))
|
.setIcon(IconCompat.createWithBitmap(conversationBitmap))
|
||||||
.setIntent(conversationIntent)
|
.setIntent(conversationIntent)
|
||||||
.setLongLived(true)
|
.setLongLived(true)
|
||||||
.setPerson(author)
|
.setPerson(author)
|
||||||
|
|
@ -154,7 +236,7 @@ class HandlerService : FirebaseMessagingService() {
|
||||||
putExtra("channelId", channelId)
|
putExtra("channelId", channelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
val action: NotificationCompat.Action =
|
val replyAction: NotificationCompat.Action =
|
||||||
NotificationCompat.Action.Builder(
|
NotificationCompat.Action.Builder(
|
||||||
R.drawable.ic_reply_24dp,
|
R.drawable.ic_reply_24dp,
|
||||||
getString(R.string.message_context_sheet_actions_reply),
|
getString(R.string.message_context_sheet_actions_reply),
|
||||||
|
|
@ -168,6 +250,24 @@ class HandlerService : FirebaseMessagingService() {
|
||||||
.addRemoteInput(remoteInput)
|
.addRemoteInput(remoteInput)
|
||||||
.build()
|
.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(
|
val contentIntent = PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
channelId.hashCode(),
|
channelId.hashCode(),
|
||||||
|
|
@ -182,11 +282,13 @@ class HandlerService : FirebaseMessagingService() {
|
||||||
.setContentIntent(contentIntent)
|
.setContentIntent(contentIntent)
|
||||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||||
.setStyle(
|
.setStyle(
|
||||||
NotificationCompat.MessagingStyle(author)
|
NotificationCompat.MessagingStyle(self)
|
||||||
|
.setGroupConversation(dbChannel?.channelType != "DirectMessage")
|
||||||
.setConversationTitle(channelName)
|
.setConversationTitle(channelName)
|
||||||
.addMessage(body, messageTimestamp, author)
|
.addMessage(body, messageTimestamp, author)
|
||||||
)
|
)
|
||||||
.addAction(action)
|
.addAction(replyAction)
|
||||||
|
.addAction(markAsReadAction)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
|
||||||
|
|
@ -204,7 +306,7 @@ class HandlerService : FirebaseMessagingService() {
|
||||||
|
|
||||||
val bubbleMetadata = NotificationCompat.BubbleMetadata.Builder(
|
val bubbleMetadata = NotificationCompat.BubbleMetadata.Builder(
|
||||||
bubbleIntent,
|
bubbleIntent,
|
||||||
IconCompat.createWithBitmap(bitmap)
|
IconCompat.createWithBitmap(conversationBitmap)
|
||||||
)
|
)
|
||||||
.setDesiredHeight(600)
|
.setDesiredHeight(600)
|
||||||
.setAutoExpandBubble(false)
|
.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.realtime.RealtimeSocket
|
||||||
import chat.stoat.api.routes.microservices.gazette.getLatestChangelog
|
import chat.stoat.api.routes.microservices.gazette.getLatestChangelog
|
||||||
import chat.stoat.api.routes.push.subscribePush
|
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.api.settings.SyncedSettings
|
||||||
import chat.stoat.callbacks.Action
|
import chat.stoat.callbacks.Action
|
||||||
import chat.stoat.callbacks.ActionChannel
|
import chat.stoat.callbacks.ActionChannel
|
||||||
|
|
@ -162,6 +165,12 @@ class ChatRouterViewModel(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
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
|
val pendingChannel = NotificationDeepLink.pendingChannelId.value
|
||||||
if (pendingChannel != null) {
|
if (pendingChannel != null) {
|
||||||
NotificationDeepLink.pendingChannelId.value = null
|
NotificationDeepLink.pendingChannelId.value = null
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ class SettingsScreenViewModel(
|
||||||
fun logout() {
|
fun logout() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
kvStorage.remove("sessionToken")
|
kvStorage.remove("sessionToken")
|
||||||
|
kvStorage.remove("selfId")
|
||||||
|
kvStorage.remove("selfName")
|
||||||
|
kvStorage.remove("selfAvatarUrl")
|
||||||
LoadedSettings.reset()
|
LoadedSettings.reset()
|
||||||
StoatAPI.logout()
|
StoatAPI.logout()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue