diff --git a/android/app/src/main/java/ru/daemonlord/messenger/core/notifications/NotificationDispatcher.kt b/android/app/src/main/java/ru/daemonlord/messenger/core/notifications/NotificationDispatcher.kt
index b7eb873..d480573 100644
--- a/android/app/src/main/java/ru/daemonlord/messenger/core/notifications/NotificationDispatcher.kt
+++ b/android/app/src/main/java/ru/daemonlord/messenger/core/notifications/NotificationDispatcher.kt
@@ -4,6 +4,8 @@ package ru.daemonlord.messenger.core.notifications
import android.Manifest
import android.app.PendingIntent
+import android.graphics.BitmapFactory
+import android.graphics.Bitmap
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -18,6 +20,7 @@ import ru.daemonlord.messenger.MainActivity
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.abs
+import java.net.URL
@Singleton
class NotificationDispatcher @Inject constructor(
@@ -32,6 +35,7 @@ class NotificationDispatcher @Inject constructor(
} else {
NotificationChannels.CHANNEL_MESSAGES
}
+ val displayBody = payload.toDisplayBody(context)
val state = synchronized(chatStates) {
val existing = chatStates[payload.chatId]
@@ -40,7 +44,7 @@ class NotificationDispatcher @Inject constructor(
}
val updated = (existing ?: ChatNotificationState(title = payload.title))
.copy(title = payload.title)
- .appendMessage(payload.body, payload.messageId)
+ .appendMessage(displayBody, payload.messageId)
chatStates[payload.chatId] = updated
updated
}
@@ -82,19 +86,21 @@ class NotificationDispatcher @Inject constructor(
)
val contentText = when {
- state.unreadCount <= 1 -> state.lines.firstOrNull() ?: payload.body
- else -> "${state.unreadCount} new messages"
+ state.unreadCount <= 1 -> state.lines.firstOrNull() ?: displayBody
+ else -> context.getString(R.string.notification_messages_count, state.unreadCount)
}
val inboxStyle = NotificationCompat.InboxStyle()
.setBigContentTitle(state.title)
- .setSummaryText("${state.unreadCount} messages")
+ .setSummaryText(context.getString(R.string.notification_messages_count, state.unreadCount))
state.lines.reversed().forEach { inboxStyle.addLine(it) }
+ val bigPicture = payload.previewImageUrl
+ ?.takeIf { it.isNotBlank() && state.unreadCount <= 1 && payload.messageType.equals("image", ignoreCase = true) }
+ ?.let { loadNotificationBitmap(it) }
- val notification = NotificationCompat.Builder(context, channelId)
+ val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification_small)
- .setContentTitle(state.title)
+ .setContentTitle(payload.senderName?.ifBlank { null } ?: state.title)
.setContentText(contentText)
- .setStyle(inboxStyle)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setGroup(GROUP_KEY_CHATS)
@@ -120,7 +126,19 @@ class NotificationDispatcher @Inject constructor(
.setAllowGeneratedReplies(true)
.build(),
)
- .build()
+ if (bigPicture != null) {
+ builder.setLargeIcon(bigPicture)
+ .setStyle(
+ NotificationCompat.BigPictureStyle()
+ .bigPicture(bigPicture)
+ .bigLargeIcon(null as Bitmap?)
+ .setBigContentTitle(payload.senderName?.ifBlank { null } ?: state.title)
+ .setSummaryText(contentText)
+ )
+ } else {
+ builder.setStyle(inboxStyle)
+ }
+ val notification = builder.build()
val manager = NotificationManagerCompat.from(context)
manager.notifySafely(chatNotificationId(payload.chatId), notification)
@@ -145,7 +163,7 @@ class NotificationDispatcher @Inject constructor(
val totalUnread = snapshot.sumOf { it.unreadCount }
val inboxStyle = NotificationCompat.InboxStyle()
.setBigContentTitle("Benya Messenger")
- .setSummaryText("$totalUnread messages")
+ .setSummaryText(context.getString(R.string.notification_messages_count, totalUnread))
snapshot.take(6).forEach { state ->
val preview = state.lines.firstOrNull().orEmpty()
val line = if (preview.isBlank()) {
@@ -158,7 +176,7 @@ class NotificationDispatcher @Inject constructor(
val summary = NotificationCompat.Builder(context, NotificationChannels.CHANNEL_MESSAGES)
.setSmallIcon(R.drawable.ic_notification_small)
.setContentTitle("Benya Messenger")
- .setContentText("$totalUnread new messages in ${snapshot.size} chats")
+ .setContentText(context.getString(R.string.notification_summary_count, totalUnread, snapshot.size))
.setStyle(inboxStyle)
.setGroup(GROUP_KEY_CHATS)
.setGroupSummary(true)
@@ -188,6 +206,35 @@ class NotificationDispatcher @Inject constructor(
return abs((chatId * 1_000_003L).toInt())
}
+ private fun loadNotificationBitmap(url: String): Bitmap? {
+ return runCatching {
+ URL(url).openStream().use { input ->
+ BitmapFactory.decodeStream(input)
+ }
+ }.getOrNull()
+ }
+
+ private fun ChatNotificationPayload.toDisplayBody(context: Context): String {
+ val normalizedType = messageType?.trim()?.lowercase()
+ val raw = body.trim()
+ if (!raw.isBlank() && normalizedType == "text") {
+ return raw
+ }
+ return when (normalizedType) {
+ "image" -> {
+ if (raw.equals("photo", ignoreCase = true) || raw.equals("image", ignoreCase = true) || raw.isBlank()) {
+ context.getString(R.string.notification_type_photo)
+ } else raw
+ }
+ "video" -> if (raw.equals("video", ignoreCase = true) || raw.isBlank()) context.getString(R.string.notification_type_video) else raw
+ "audio" -> if (raw.equals("audio", ignoreCase = true) || raw.isBlank()) context.getString(R.string.notification_type_audio) else raw
+ "voice" -> if (raw.equals("voice message", ignoreCase = true) || raw.isBlank()) context.getString(R.string.notification_type_voice) else raw
+ "circle_video" -> if (raw.equals("video note", ignoreCase = true) || raw.isBlank()) context.getString(R.string.notification_type_circle) else raw
+ "file" -> if (raw.equals("file", ignoreCase = true) || raw.isBlank()) context.getString(R.string.notification_type_file) else raw
+ else -> raw.ifBlank { context.getString(R.string.notification_type_message) }
+ }
+ }
+
private data class ChatNotificationState(
val title: String,
val unreadCount: Int = 0,
@@ -195,7 +242,7 @@ class NotificationDispatcher @Inject constructor(
val lastMessageId: Long? = null,
) {
fun appendMessage(body: String, messageId: Long?): ChatNotificationState {
- val normalized = body.trim().ifBlank { "New message" }
+ val normalized = body.trim().ifBlank { "Message" }
val updatedLines = buildList {
add(normalized)
lines.forEach { add(it) }
diff --git a/android/app/src/main/java/ru/daemonlord/messenger/core/notifications/NotificationModels.kt b/android/app/src/main/java/ru/daemonlord/messenger/core/notifications/NotificationModels.kt
index eaad261..ab47251 100644
--- a/android/app/src/main/java/ru/daemonlord/messenger/core/notifications/NotificationModels.kt
+++ b/android/app/src/main/java/ru/daemonlord/messenger/core/notifications/NotificationModels.kt
@@ -6,6 +6,9 @@ data class ChatNotificationPayload(
val title: String,
val body: String,
val isMention: Boolean = false,
+ val messageType: String? = null,
+ val previewImageUrl: String? = null,
+ val senderName: String? = null,
)
object NotificationIntentExtras {
diff --git a/android/app/src/main/java/ru/daemonlord/messenger/push/PushPayloadParser.kt b/android/app/src/main/java/ru/daemonlord/messenger/push/PushPayloadParser.kt
index 69b7bbb..e448e28 100644
--- a/android/app/src/main/java/ru/daemonlord/messenger/push/PushPayloadParser.kt
+++ b/android/app/src/main/java/ru/daemonlord/messenger/push/PushPayloadParser.kt
@@ -38,6 +38,9 @@ object PushPayloadParser {
title = title,
body = body,
isMention = isMention,
+ messageType = data["message_type"] ?: data["messageType"],
+ previewImageUrl = data["preview_image_url"] ?: data["previewImageUrl"],
+ senderName = data["sender_name"] ?: data["senderName"],
)
}
}
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index f48c7a0..76c3636 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -83,6 +83,15 @@
Ответить
Введите ответ
Прочитано
+ %1$d сообщений
+ %1$d новых сообщений в %2$d чатах
+ Сообщение
+ Фото
+ Видео
+ Аудио
+ Голосовое
+ Кружок
+ Файл
Аватар
Пользователь
Выбрать фото
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 9d1e2ab..33c0c7d 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -83,6 +83,15 @@
Reply
Write a reply
Mark as read
+ %1$d messages
+ %1$d new messages in %2$d chats
+ Message
+ Photo
+ Video
+ Audio
+ Voice message
+ Video note
+ File
Avatar
User
Choose photo
diff --git a/app/notifications/service.py b/app/notifications/service.py
index 1ef8a91..dce6ac2 100644
--- a/app/notifications/service.py
+++ b/app/notifications/service.py
@@ -4,6 +4,7 @@ import re
from sqlalchemy.ext.asyncio import AsyncSession
from app.chats.repository import is_chat_muted_for_user, list_chat_members
+from app.media.repository import list_attachments_by_message_ids
from app.messages.models import Message
from app.notifications.repository import (
create_notification_log,
@@ -29,6 +30,21 @@ def _extract_mentions(text: str | None) -> set[str]:
return {match.group(1).lower() for match in _MENTION_RE.finditer(text)}
+def _message_preview_label(message: Message, image_preview_url: str | None) -> str:
+ text_preview = (message.text or "").strip()
+ if text_preview:
+ return text_preview[:120]
+ message_type = message.type.value if hasattr(message.type, "value") else str(message.type)
+ return {
+ "image": "Photo" if image_preview_url else "Image",
+ "video": "Video",
+ "audio": "Audio",
+ "voice": "Voice message",
+ "circle_video": "Video note",
+ "file": "File",
+ }.get(message_type, "Message")
+
+
async def enqueue_notification(db: AsyncSession, payload: NotificationRequest) -> None:
await create_notification_log(
db,
@@ -51,18 +67,28 @@ async def dispatch_message_notifications(db: AsyncSession, message: Message) ->
sender_users = await list_users_by_ids(db, [message.sender_id])
sender_name = sender_users[0].username if sender_users else "Someone"
+ attachments = await list_attachments_by_message_ids(db, message_ids=[message.id])
+ first_attachment = attachments[0] if attachments else None
+ preview_image_url = None
+ if first_attachment and first_attachment.file_type.lower().startswith("image/"):
+ preview_image_url = first_attachment.file_url
+ preview_body = _message_preview_label(message, preview_image_url=preview_image_url)
+ message_type = message.type.value if hasattr(message.type, "value") else str(message.type)
for recipient in users:
base_payload = {
"chat_id": message.chat_id,
"message_id": message.id,
"sender_id": message.sender_id,
+ "message_type": message_type,
+ "preview_image_url": preview_image_url or "",
+ "sender_name": sender_name,
}
if recipient.id in mentioned_user_ids:
payload = {
**base_payload,
"type": "mention",
- "text_preview": (message.text or "")[:120],
+ "text_preview": preview_body,
}
await create_notification_log(
db,
@@ -72,8 +98,8 @@ async def dispatch_message_notifications(db: AsyncSession, message: Message) ->
)
send_mention_notification_task.delay(
recipient.id,
- f"{sender_name} mentioned you",
- (message.text or "")[:120],
+ sender_name,
+ preview_body,
payload,
)
continue
@@ -84,7 +110,7 @@ async def dispatch_message_notifications(db: AsyncSession, message: Message) ->
payload = {
**base_payload,
"type": "message",
- "text_preview": (message.text or "")[:120],
+ "text_preview": preview_body,
}
await create_notification_log(
db,
@@ -94,8 +120,8 @@ async def dispatch_message_notifications(db: AsyncSession, message: Message) ->
)
send_push_notification_task.delay(
recipient.id,
- f"New message from {sender_name}",
- (message.text or "")[:120],
+ sender_name,
+ preview_body,
payload,
)