diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea6add7d..0647d4d1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -66,6 +66,10 @@ + + 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 } -} \ No newline at end of file +} diff --git a/app/src/main/java/chat/stoat/c2dm/NotificationDeepLink.kt b/app/src/main/java/chat/stoat/c2dm/NotificationDeepLink.kt new file mode 100644 index 00000000..43ba9ec5 --- /dev/null +++ b/app/src/main/java/chat/stoat/c2dm/NotificationDeepLink.kt @@ -0,0 +1,7 @@ +package chat.stoat.c2dm + +import kotlinx.coroutines.flow.MutableStateFlow + +object NotificationDeepLink { + val pendingChannelId = MutableStateFlow(null) +} diff --git a/app/src/main/java/chat/stoat/c2dm/ReplyReceiver.kt b/app/src/main/java/chat/stoat/c2dm/ReplyReceiver.kt new file mode 100644 index 00000000..448f57ca --- /dev/null +++ b/app/src/main/java/chat/stoat/c2dm/ReplyReceiver.kt @@ -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) + } +} diff --git a/app/src/main/java/chat/stoat/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/stoat/screens/chat/ChatRouterScreen.kt index 93f68688..bc7b65e3 100644 --- a/app/src/main/java/chat/stoat/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/stoat/screens/chat/ChatRouterScreen.kt @@ -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") diff --git a/app/src/main/res/drawable/ic_stoat_24dp.xml b/app/src/main/res/drawable/ic_stoat_24dp.xml new file mode 100644 index 00000000..08fc9c97 --- /dev/null +++ b/app/src/main/res/drawable/ic_stoat_24dp.xml @@ -0,0 +1,10 @@ + + +