diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index bdf0cb3..93af5a0 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -386,7 +386,8 @@ fun ChatScreen( val senderNameByUserId = remember(state.messages, state.chatMembers) { val fromMembers = state.chatMembers.associate { member -> val resolved = member.name.ifBlank { - member.username?.takeIf { it.isNotBlank() }?.let { "@$it" } ?: "User #${member.userId}" + member.username?.takeIf { it.isNotBlank() }?.let { "@$it" } + ?: context.getString(R.string.chat_user_fallback_with_id, member.userId) } member.userId to resolved } @@ -694,7 +695,9 @@ fun ChatScreen( } Column(modifier = Modifier.weight(1f)) { Text( - text = state.chatTitle.ifBlank { "Chat #${state.chatId}" }, + text = state.chatTitle.ifBlank { + stringResource(id = R.string.chat_title_fallback, state.chatId) + }, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, maxLines = 1, @@ -1287,7 +1290,9 @@ fun ChatScreen( } Column(modifier = Modifier.weight(1f)) { Text( - text = state.chatTitle.ifBlank { "Chat #${state.chatId}" }, + text = state.chatTitle.ifBlank { + stringResource(id = R.string.chat_title_fallback, state.chatId) + }, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, ) @@ -1977,7 +1982,7 @@ fun ChatScreen( .clickable { emojiPickerTab = tab }, ) { Text( - text = tab.title, + text = stringResource(id = tab.titleRes), modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), style = MaterialTheme.typography.labelLarge, ) @@ -2170,6 +2175,7 @@ private fun MessageBubble( onClick: () -> Unit, onLongPress: () -> Unit, ) { + val context = LocalContext.current val isOutgoing = message.isOutgoing val renderAsChannelPost = isChannelChat val alignAsOutgoing = isOutgoing && !renderAsChannelPost @@ -2360,7 +2366,11 @@ private fun MessageBubble( if (isLegacyImageUrlMessage) { val isLegacyStickerMessage = isStickerAsset(fileUrl = textUrl, fileType = "image/url") - val badgeLabel = if (isLegacyStickerMessage) null else mediaBadgeLabel(fileType = "image/url", url = textUrl) + val badgeLabel = if (isLegacyStickerMessage) { + null + } else { + mediaBadgeLabel(fileType = "image/url", url = textUrl, context = context) + } val openable = !isLegacyStickerMessage && badgeLabel == null Box( modifier = Modifier @@ -2434,7 +2444,11 @@ private fun MessageBubble( if (imageAttachments.size == 1) { val single = imageAttachments.first() val isStickerImage = isStickerAsset(fileUrl = single.fileUrl, fileType = single.fileType) - val badgeLabel = if (isStickerImage) null else mediaBadgeLabel(fileType = single.fileType, url = single.fileUrl) + val badgeLabel = if (isStickerImage) { + null + } else { + mediaBadgeLabel(fileType = single.fileType, url = single.fileUrl, context = context) + } val openable = !isStickerImage && badgeLabel == null Box( modifier = Modifier @@ -2475,7 +2489,11 @@ private fun MessageBubble( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { rowItems.forEach { image -> - val badgeLabel = mediaBadgeLabel(fileType = image.fileType, url = image.fileUrl) + val badgeLabel = mediaBadgeLabel( + fileType = image.fileType, + url = image.fileUrl, + context = context, + ) val openable = badgeLabel == null Box( modifier = Modifier @@ -2533,9 +2551,15 @@ private fun MessageBubble( playbackTitle = message.senderDisplayName?.ifBlank { null } ?: extractFileName(attachment.fileUrl), playbackSubtitle = if (message.type.contains("voice", ignoreCase = true)) { - "Voice message • ${formatMessageTime(message.createdAt)}" + stringResource( + id = R.string.chat_playback_subtitle_voice, + formatMessageTime(message.createdAt), + ) } else { - "Audio • ${formatMessageTime(message.createdAt)}" + stringResource( + id = R.string.chat_playback_subtitle_audio, + formatMessageTime(message.createdAt), + ) }, messageId = message.id, onPlaybackChanged = onAudioPlaybackChanged, @@ -2741,10 +2765,10 @@ private sealed interface ChatTimelineItem { ) : ChatTimelineItem } -private enum class ComposerPickerTab(val title: String) { - Emoji("Эмодзи"), - Gif("GIF"), - Sticker("Стикеры"), +private enum class ComposerPickerTab(@StringRes val titleRes: Int) { + Emoji(R.string.chat_picker_tab_emoji), + Gif(R.string.chat_picker_tab_gif), + Sticker(R.string.chat_picker_tab_stickers), } private data class RemotePickerItem( @@ -3882,12 +3906,19 @@ private fun ChatMembersTabContent( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = member.name.ifBlank { member.username?.let { "@$it" } ?: "User #${member.userId}" }, + text = member.name.ifBlank { + member.username?.let { "@$it" } + ?: stringResource(id = R.string.chat_user_fallback_with_id, member.userId) + }, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, ) Text( - text = "${member.role} • id ${member.userId}", + text = stringResource( + id = R.string.chat_member_role_with_id, + member.role, + member.userId, + ), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -3993,12 +4024,15 @@ private fun ChatMembersTabContent( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = ban.name.ifBlank { ban.username?.let { "@$it" } ?: "User #${ban.userId}" }, + text = ban.name.ifBlank { + ban.username?.let { "@$it" } + ?: stringResource(id = R.string.chat_user_fallback_with_id, ban.userId) + }, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, ) Text( - text = "id ${ban.userId}", + text = stringResource(id = R.string.chat_member_id, ban.userId), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -4115,11 +4149,15 @@ private fun isStickerAsset(fileUrl: String, fileType: String): Boolean { return isStickerLikeUrl(fileUrl) } -private fun mediaBadgeLabel(fileType: String, url: String): String? { +private fun mediaBadgeLabel( + fileType: String, + url: String, + context: Context, +): String? { val normalizedType = fileType.lowercase(Locale.getDefault()) return when { - normalizedType.contains("gif") || isGifLikeUrl(url) -> "GIF" - normalizedType.contains("webp") || isStickerLikeUrl(url) -> "Sticker" + normalizedType.contains("gif") || isGifLikeUrl(url) -> context.getString(R.string.chat_media_badge_gif) + normalizedType.contains("webp") || isStickerLikeUrl(url) -> context.getString(R.string.chat_media_badge_sticker) else -> null } } diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index a740944..b4ab89c 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -147,6 +147,16 @@ Кикнуть Разбанить Видео + GIF + Стикер + Эмодзи + GIF + Стикеры + Голосовое сообщение • %1$s + Аудио • %1$s + Пользователь #%1$d + %1$s • id %2$d + id %1$d Понизить админа Понизить %1$s до участника? Передача owner diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 438d294..4b6071f 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -147,6 +147,16 @@ Kick Unban Video + GIF + Sticker + Emoji + GIF + Stickers + Voice message • %1$s + Audio • %1$s + User #%1$d + %1$s • id %2$d + id %1$d Demote admin Demote %1$s to member? Transfer ownership