1.0.0.3更新1.语音发送支持2.繁体中文支持3.通知音效支持4.修复部分多语言翻译错误

pull/14/head
minxiwan 1 week ago
parent 0404e665c1
commit 82c4d9b6e3

@ -0,0 +1,20 @@
作品Notification sound effects
来源ear0
作者Risteard
许可CC-BY 保留署名许可协议
描述:通知铃声。
-------------------------------------------------
作品load
来源ear0
作者weidu27
许可CC0 公众共享许可协议
描述:叮咚
-------------------------------------------------
作品nameit5
来源Freesound
作者bumpelsnake
许可CC-BY 保留署名许可协议
描述some chime, maybe a notification sound
-------------------------------------------------
Thank you to all the creators mentioned above.
感谢上述所有创作者。

@ -14,7 +14,7 @@ android {
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0.0.2"
versionName = "1.0.0.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

@ -24,7 +24,8 @@ data class UserPreferences(
val shouldAutoReconnect: Boolean,
val themeId: String = "blue",
val useDynamicColor: Boolean = true,
val language: String = "zh" // 默认中文
val language: String = "zh",
val notificationSound: String = "default"
)
class UserPreferencesRepository(
@ -50,7 +51,8 @@ class UserPreferencesRepository(
shouldAutoReconnect = prefs[KEY_SHOULD_AUTO_RECONNECT] ?: false,
themeId = prefs[KEY_THEME_ID] ?: "blue",
useDynamicColor = prefs[KEY_USE_DYNAMIC_COLOR] ?: true,
language = prefs[KEY_LANGUAGE] ?: "zh"
language = prefs[KEY_LANGUAGE] ?: "zh",
notificationSound = prefs[KEY_NOTIFICATION_SOUND] ?: "default"
)
}
@ -66,6 +68,12 @@ class UserPreferencesRepository(
}
}
suspend fun setNotificationSound(sound: String) {
context.dataStore.edit { prefs ->
prefs[KEY_NOTIFICATION_SOUND] = sound
}
}
suspend fun setUseDynamicColor(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[KEY_USE_DYNAMIC_COLOR] = enabled
@ -155,5 +163,6 @@ class UserPreferencesRepository(
val KEY_THEME_ID: Preferences.Key<String> = stringPreferencesKey("theme_id")
val KEY_USE_DYNAMIC_COLOR: Preferences.Key<Boolean> = booleanPreferencesKey("use_dynamic_color")
val KEY_LANGUAGE: Preferences.Key<String> = stringPreferencesKey("language")
val KEY_NOTIFICATION_SOUND: Preferences.Key<String> = stringPreferencesKey("notification_sound")
}
}

@ -33,6 +33,7 @@ import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Forum
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button
import androidx.compose.material3.Card
@ -213,7 +214,8 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) {
onClearMessages = viewModel::clearMessages,
onThemeChange = viewModel::updateTheme,
onUseDynamicColorChange = viewModel::updateUseDynamicColor,
onLanguageChange = viewModel::updateLanguage
onLanguageChange = viewModel::updateLanguage,
onNotificationSoundChange = viewModel::updateNotificationSound
)
}
}
@ -577,6 +579,7 @@ private fun MessageItem(
* @param onThemeChange 切换主题
* @param onUseDynamicColorChange 切换动态颜色
* @param onLanguageChange 切换语言
* @param onNotificationSoundChange 切换通知音效
*/
@Composable
private fun SettingsTab(
@ -593,7 +596,8 @@ private fun SettingsTab(
onClearMessages: () -> Unit,
onThemeChange: (String) -> Unit,
onUseDynamicColorChange: (Boolean) -> Unit,
onLanguageChange: (String) -> Unit
onLanguageChange: (String) -> Unit,
onNotificationSoundChange: (String) -> Unit
) {
fun language(key: String) = LanguageManager.getString(key, state.language)
@ -736,6 +740,39 @@ private fun SettingsTab(
}
}
item {
Card(modifier = settingsCardModifier) {
Column(
modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing
) {
Text(language("settings.notification_sound"), style = MaterialTheme.typography.titleMedium)
val sounds = listOf(
"default" to language("sound.default"),
"ding" to language("sound.ding"),
"nameit5" to language("sound.nameit5"),
"wind_chime" to language("sound.wind_chime")
)
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(sounds) { (id, label) ->
FilterChip(
selected = state.notificationSound == id,
onClick = { onNotificationSoundChange(id) },
label = { Text(label) },
leadingIcon = {
Icon(
Icons.Rounded.MusicNote,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
)
}
}
}
}
}
item {
Card(modifier = settingsCardModifier) {
Column(

@ -5,9 +5,12 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -52,7 +55,6 @@ import com.onlinemsg.client.util.LanguageManager
/**
* 单例管理类负责整个聊天会话的生命周期网络连接消息收发状态维护和持久化
* 所有公开方法均通过 ViewModel 代理调用内部使用协程处理异步操作
*/
object ChatSessionManager {
@ -81,18 +83,18 @@ object ChatSessionManager {
private var identity: RsaCryptoManager.Identity? = null
// 连接相关内部状态
private var manualClose = false // 是否为手动断开
private var fallbackTried = false // 是否已尝试切换 ws/wss
private var connectedUrl = "" // 当前连接的服务器地址
private var serverPublicKey = "" // 服务端公钥(握手后获得)
private var helloTimeoutJob: Job? = null // 握手超时任务
private var authTimeoutJob: Job? = null // 认证超时任务
private var reconnectJob: Job? = null // 自动重连任务
private var reconnectAttempt: Int = 0 // 当前重连尝试次数
private val systemMessageExpiryJobs: MutableMap<String, Job> = mutableMapOf() // 系统消息自动过期任务
private var manualClose = false
private var fallbackTried = false
private var connectedUrl = ""
private var serverPublicKey = ""
private var helloTimeoutJob: Job? = null
private var authTimeoutJob: Job? = null
private var reconnectJob: Job? = null
private var reconnectAttempt: Int = 0
private val systemMessageExpiryJobs: MutableMap<String, Job> = mutableMapOf()
private var autoReconnectTriggered = false
@Volatile
private var keepAliveRequested = false // 是否应保活(前台服务标志)
private var keepAliveRequested = false
private var notificationIdSeed = 2000
// WebSocket 事件监听器
@ -167,10 +169,6 @@ object ChatSessionManager {
}
}
/**
* 初始化管理器必须在应用启动时调用一次
* @param application Application 实例
*/
@Synchronized
fun initialize(application: Application) {
if (initialized) return
@ -179,7 +177,6 @@ object ChatSessionManager {
preferencesRepository = UserPreferencesRepository(application, json)
cryptoManager = RsaCryptoManager(application)
historyRepository = ChatHistoryRepository(ChatDatabase.getInstance(application).chatMessageDao())
ensureMessageNotificationChannel()
scope.launch {
val pref = preferencesRepository.preferencesFlow.first()
@ -197,11 +194,14 @@ object ChatSessionManager {
themeId = pref.themeId,
useDynamicColor = pref.useDynamicColor,
language = pref.language,
notificationSound = pref.notificationSound,
messages = historyMessages,
statusHint = LanguageManager.getString("status_hint.click_to_connect", pref.language)
)
}
// 如果上次会话启用了自动重连,则自动恢复连接
ensureMessageNotificationChannel(pref.notificationSound)
if (pref.shouldAutoReconnect && !autoReconnectTriggered) {
autoReconnectTriggered = true
ChatForegroundService.start(application)
@ -210,10 +210,14 @@ object ChatSessionManager {
}
}
/**
* 更新主题
* @param themeId 主题名
*/
fun updateNotificationSound(sound: String) {
_uiState.update { it.copy(notificationSound = sound) }
scope.launch {
preferencesRepository.setNotificationSound(sound)
ensureMessageNotificationChannel(sound)
}
}
fun updateTheme(themeId: String) {
_uiState.update { it.copy(themeId = themeId) }
scope.launch {
@ -221,10 +225,6 @@ object ChatSessionManager {
}
}
/**
* 更新语言
* @param language 语言代码
*/
fun updateLanguage(language: String) {
_uiState.update { it.copy(language = language) }
scope.launch {
@ -232,10 +232,6 @@ object ChatSessionManager {
}
}
/**
* 更改使用动态颜色
* @param enabled 主题名
*/
fun updateUseDynamicColor(enabled: Boolean) {
_uiState.update { it.copy(useDynamicColor = enabled) }
scope.launch {
@ -243,10 +239,6 @@ object ChatSessionManager {
}
}
/**
* 更新显示名称并持久化
* @param value 新名称自动截断至 64 字符
*/
fun updateDisplayName(value: String) {
val displayName = value.take(64)
_uiState.update { it.copy(displayName = displayName) }
@ -255,34 +247,18 @@ object ChatSessionManager {
}
}
/**
* 更新当前输入的服务器地址不持久化
* @param value 新地址
*/
fun updateServerUrl(value: String) {
_uiState.update { it.copy(serverUrl = value) }
}
/**
* 更新私聊目标公钥
* @param value 公钥字符串
*/
fun updateTargetKey(value: String) {
_uiState.update { it.copy(targetKey = value) }
}
/**
* 更新消息草稿
* @param value 草稿内容
*/
fun updateDraft(value: String) {
_uiState.update { it.copy(draft = value) }
}
/**
* 切换广播/私聊模式并持久化
* @param enabled true 为私聊模式
*/
fun toggleDirectMode(enabled: Boolean) {
_uiState.update { it.copy(directMode = enabled) }
scope.launch {
@ -290,10 +266,6 @@ object ChatSessionManager {
}
}
/**
* 切换是否显示系统消息并持久化
* @param show true 显示
*/
fun toggleShowSystemMessages(show: Boolean) {
_uiState.update { it.copy(showSystemMessages = show) }
scope.launch {
@ -301,9 +273,6 @@ object ChatSessionManager {
}
}
/**
* 清空所有消息并取消系统消息的过期任务
*/
fun clearMessages() {
cancelSystemMessageExpiryJobs()
_uiState.update { it.copy(messages = emptyList()) }
@ -314,9 +283,6 @@ object ChatSessionManager {
}
}
/**
* 保存当前服务器地址到历史列表并持久化
*/
fun saveCurrentServerUrl() {
val normalized = ServerUrlFormatter.normalize(_uiState.value.serverUrl)
if (normalized.isBlank()) {
@ -342,10 +308,6 @@ object ChatSessionManager {
}
}
/**
* 从历史列表中移除当前服务器地址
* 如果列表清空则恢复默认地址
*/
fun removeCurrentServerUrl() {
val normalized = ServerUrlFormatter.normalize(_uiState.value.serverUrl)
if (normalized.isBlank()) return
@ -376,9 +338,6 @@ object ChatSessionManager {
}
}
/**
* 加载或生成本地身份密钥对并将公钥显示到 UI
*/
fun revealMyPublicKey() {
scope.launch {
_uiState.update { it.copy(loadingPublicKey = true) }
@ -398,17 +357,10 @@ object ChatSessionManager {
}
}
/**
* 主动连接服务器由用户点击连接触发
*/
fun connect() {
connectInternal(isAutoRestore = false)
}
/**
* 内部连接逻辑区分自动恢复和手动连接
* @param isAutoRestore 是否为应用启动时的自动恢复连接
*/
private fun connectInternal(isAutoRestore: Boolean) {
if (!initialized) return
val state = _uiState.value
@ -459,10 +411,6 @@ object ChatSessionManager {
}
}
/**
* 主动断开连接
* @param stopService 是否同时停止前台服务默认 true
*/
fun disconnect(stopService: Boolean = true) {
manualClose = true
cancelReconnect()
@ -487,10 +435,6 @@ object ChatSessionManager {
addSystemMessage("已断开连接")
}
/**
* 发送消息广播或私聊
* 执行签名加密并发送
*/
fun sendMessage() {
val current = _uiState.value
if (!current.canSend) return
@ -549,19 +493,12 @@ object ChatSessionManager {
}
}
/**
* 消息复制成功后的回调显示已复制提示
*/
fun onMessageCopied() {
scope.launch {
_events.emit(UiEvent.ShowSnackbar("已复制"))
}
}
/**
* 确保本地身份已加载 or 创建
* @return 本地身份对象
*/
private suspend fun ensureIdentity(): RsaCryptoManager.Identity {
return identityMutex.withLock {
identity ?: withContext(Dispatchers.Default) {
@ -572,10 +509,6 @@ object ChatSessionManager {
}
}
/**
* 处理收到的原始文本消息可能是握手包 or 加密消息
* @param rawText 原始文本
*/
private suspend fun handleIncomingMessage(rawText: String) {
if (_uiState.value.status == ConnectionStatus.HANDSHAKING) {
val lang = _uiState.value.language
@ -587,7 +520,6 @@ object ChatSessionManager {
json.decodeFromString<JsonElement>(normalizedText) as? JsonObject
}.getOrNull()
// 尝试直接解析为 HelloDataDto某些服务器可能直接发送不带外层
val directHello = rootObject?.let { obj ->
val hasPublicKey = obj["publicKey"] != null
val hasChallenge = obj["authChallenge"] != null
@ -603,7 +535,6 @@ object ChatSessionManager {
return
}
// 尝试解析为带外层的 EnvelopeDto
val plain = runCatching { json.decodeFromString<EnvelopeDto>(normalizedText) }.getOrNull()
if (plain?.type == "publickey") {
cancelHelloTimeout()
@ -624,7 +555,6 @@ object ChatSessionManager {
return
}
// 握手阶段收到非预期消息则报错
if (_uiState.value.status == ConnectionStatus.HANDSHAKING && plain != null) {
val lang = _uiState.value.language
_uiState.update { it.copy(statusHint = LanguageManager.getString("status_hint.handshake_failed_unexpected", lang)) }
@ -636,7 +566,6 @@ object ChatSessionManager {
addSystemMessage("握手包解析失败:$preview")
}
// 尝试解密(若已握手完成,收到的应是加密消息)
val id = ensureIdentity()
val decrypted = runCatching {
withContext(Dispatchers.Default) {
@ -654,10 +583,6 @@ object ChatSessionManager {
handleSecureMessage(secure)
}
/**
* 处理服务端发来的握手 Hello 数据
* @param hello 服务端公钥和挑战
*/
private suspend fun handleServerHello(hello: HelloDataDto) {
cancelHelloTimeout()
serverPublicKey = hello.publicKey
@ -704,10 +629,6 @@ object ChatSessionManager {
}
}
/**
* 发送认证消息包含签名后的身份信息
* @param challenge 服务端提供的挑战值
*/
private suspend fun sendAuth(challenge: String) {
val id = ensureIdentity()
val displayName = _uiState.value.displayName.trim().ifBlank { createGuestName() }
@ -751,10 +672,6 @@ object ChatSessionManager {
check(socketClient.send(cipher)) { "连接不可用" }
}
/**
* 处理安全通道建立后的业务消息广播私聊认证结果等
* @param message 解密后的 EnvelopeDto
*/
private fun handleSecureMessage(message: EnvelopeDto) {
when (message.type) {
"auth_ok" -> {
@ -795,30 +712,15 @@ object ChatSessionManager {
}
}
/**
* 处理 WebSocket 连接关闭事件
* @param code 关闭状态码
* @param reason 关闭原因
*/
private fun handleSocketClosed(code: Int, reason: String) {
cancelHelloTimeout()
cancelAuthTimeout()
if (manualClose) {
return
}
if (reason == "reconnect") {
return
}
if (reconnectJob?.isActive == true) {
return
}
if (manualClose || reason == "reconnect" || reconnectJob?.isActive == true) return
val currentStatus = _uiState.value.status
val allowFallback = !fallbackTried && currentStatus != ConnectionStatus.READY
// 尝试切换 ws/wss 协议重试(仅限非就绪状态)
if (allowFallback) {
val fallbackUrl = ServerUrlFormatter.toggleWsProtocol(connectedUrl)
if (fallbackUrl.isNotBlank()) {
@ -847,13 +749,8 @@ object ChatSessionManager {
}
addSystemMessage("连接关闭 ($code)${reason.ifBlank { "连接中断" }}")
scheduleReconnect("连接已中断")
}
/**
* 添加一条系统消息自动按 TTL 过期
* @param content 消息内容
*/
private fun addSystemMessage(content: String) {
val message = UiMessage(
role = MessageRole.SYSTEM,
@ -866,13 +763,6 @@ object ChatSessionManager {
scheduleSystemMessageExpiry(message.id)
}
/**
* 添加一条接收到的用户消息
* @param sender 发送者名称
* @param subtitle 附加说明如私聊来源
* @param content 消息内容
* @param channel 消息通道广播/私聊
*/
private fun addIncomingMessage(
sender: String,
subtitle: String,
@ -894,12 +784,6 @@ object ChatSessionManager {
)
}
/**
* 添加一条发出的消息
* @param content 消息内容
* @param subtitle 附加说明如私聊目标
* @param channel 消息通道
*/
private fun addOutgoingMessage(
content: String,
subtitle: String,
@ -916,15 +800,11 @@ object ChatSessionManager {
)
}
/**
* 将消息追加到列表尾部并清理超出数量限制的消息
* @param message 要追加的消息
*/
private fun appendMessage(message: UiMessage) {
_uiState.update { current ->
val next = (current.messages + message).takeLast(MAX_MESSAGES)
val aliveIds = next.asSequence().map { it.id }.toSet()
val removedIds = systemMessageExpiryJobs.keys.filterNot { it in aliveIds }
val removedIds = systemMessageExpiryJobs.keys.filterNot { id -> id in aliveIds }
removedIds.forEach { id ->
systemMessageExpiryJobs.remove(id)?.cancel()
}
@ -938,21 +818,13 @@ object ChatSessionManager {
}
}
/**
* 取消认证超时任务
*/
private fun cancelAuthTimeout() {
authTimeoutJob?.cancel()
authTimeoutJob = null
}
/**
* 安排自动重连指数退避
* @param reason 触发重连的原因
*/
private fun scheduleReconnect(reason: String) {
if (manualClose) return
if (reconnectJob?.isActive == true) return
if (manualClose || reconnectJob?.isActive == true) return
reconnectAttempt += 1
val exponential = 1 shl minOf(reconnectAttempt - 1, 5)
@ -1004,18 +876,11 @@ object ChatSessionManager {
}
}
/**
* 取消自动重连任务
*/
private fun cancelReconnect() {
reconnectJob?.cancel()
reconnectJob = null
}
/**
* 为系统消息安排过期自动删除
* @param messageId 消息唯一 ID
*/
private fun scheduleSystemMessageExpiry(messageId: String) {
systemMessageExpiryJobs.remove(messageId)?.cancel()
systemMessageExpiryJobs[messageId] = scope.launch {
@ -1028,17 +893,11 @@ object ChatSessionManager {
}
}
/**
* 取消所有系统消息的过期任务
*/
private fun cancelSystemMessageExpiryJobs() {
systemMessageExpiryJobs.values.forEach { it.cancel() }
systemMessageExpiryJobs.clear()
}
/**
* 启动握手超时计时器
*/
private fun startHelloTimeout() {
cancelHelloTimeout()
helloTimeoutJob = scope.launch {
@ -1058,56 +917,29 @@ object ChatSessionManager {
}
}
/**
* 取消握手超时任务
*/
private fun cancelHelloTimeout() {
helloTimeoutJob?.cancel()
helloTimeoutJob = null
}
/**
* 缩写显示公钥取前后各8字符
* @param key 完整公钥
* @return 缩写字符串
*/
private fun summarizeKey(key: String): String {
if (key.length <= 16) return key
return "${key.take(8)}...${key.takeLast(8)}"
}
/**
* 生成访客名称 guest-123456
* @return 随机名称
*/
private fun createGuestName(): String {
val rand = (100000..999999).random()
return "guest-$rand"
}
/**
* 从可能包含前缀的原始文本中提取 JSON 对象部分
* @param rawText 原始文本
* @return 最外层的 JSON 字符串
*/
private fun extractJsonCandidate(rawText: String): String {
val trimmed = rawText.trim()
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
return trimmed
}
if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed
val start = rawText.indexOf('{')
val end = rawText.lastIndexOf('}')
return if (start in 0 until end) {
rawText.substring(start, end + 1)
} else {
rawText
}
return if (start in 0 until end) rawText.substring(start, end + 1) else rawText
}
/**
* 关闭所有资源用于应用退出时
*/
fun shutdownAll() {
cancelSystemMessageExpiryJobs()
cancelReconnect()
@ -1116,9 +948,6 @@ object ChatSessionManager {
socketClient.shutdown()
}
/**
* 前台服务停止时的回调
*/
fun onForegroundServiceStopped() {
keepAliveRequested = false
if (_uiState.value.status != ConnectionStatus.IDLE) {
@ -1130,52 +959,57 @@ object ChatSessionManager {
}
}
/**
* 判断前台服务是否应该运行
* @return true 表示应保持服务运行
*/
fun shouldForegroundServiceRun(): Boolean = keepAliveRequested
/**
* 创建消息通知渠道Android O+
*/
private fun ensureMessageNotificationChannel() {
private fun ensureMessageNotificationChannel(soundCode: String = "default") {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelId = "${MESSAGE_CHANNEL_ID}_$soundCode"
if (manager.getNotificationChannel(channelId) != null) return
val soundUri = getSoundUri(soundCode)
val channel = NotificationChannel(
MESSAGE_CHANNEL_ID,
channelId,
"OnlineMsg 消息提醒",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "收到服务器新消息时提醒"
if (soundUri != null) {
setSound(soundUri, AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build())
}
}
manager.createNotificationChannel(channel)
}
/**
* 显示新消息到达的通知
* @param title 通知标题
* @param body 通知正文
*/
private fun getSoundUri(code: String): Uri? {
val resId = when(code) {
"ding" -> R.raw.load
"nameit5" -> R.raw.nameit5
"wind_chime" -> R.raw.notification_sound_effects
"default" -> R.raw.default_sound
else -> return null
}
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + app.packageName + "/" + resId)
}
private fun showIncomingNotification(title: String, body: String) {
if (!initialized) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(app, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
return
}
) return
val launchIntent = Intent(app, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
app,
0,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val pendingIntent = PendingIntent.getActivity(app, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val soundCode = _uiState.value.notificationSound
val channelId = "${MESSAGE_CHANNEL_ID}_$soundCode"
val notification = NotificationCompat.Builder(app, MESSAGE_CHANNEL_ID)
val notification = NotificationCompat.Builder(app, channelId)
.setSmallIcon(android.R.drawable.stat_notify_chat)
.setContentTitle(title.ifBlank { "OnlineMsg" })
.setContentText(body.take(120))
@ -1188,17 +1022,12 @@ object ChatSessionManager {
NotificationManagerCompat.from(app).notify(nextMessageNotificationId(), notification)
}
/**
* 生成下一个通知 ID线程安全递增
* @return 新的通知 ID
*/
@Synchronized
private fun nextMessageNotificationId(): Int {
notificationIdSeed += 1
return notificationIdSeed
}
// 常量定义
private const val HELLO_TIMEOUT_MS = 12_000L
private const val AUTH_TIMEOUT_MS = 20_000L
private const val MAX_MESSAGES = 500

@ -70,7 +70,10 @@ data class UiMessage(
* @property myPublicKey 本地公钥
* @property sending 是否正在发送消息用于禁用按钮
* @property loadingPublicKey 是否正在加载公钥
* @property language 当前选择的语言代码 ( "zh", "en", "ja")
* @property themeId 当前选中的主题 ID
* @property useDynamicColor 是否使用 Android 12+ 动态颜色
* @property language 当前选择的语言代码
* @property notificationSound 当前选择的通知音效代号
*/
data class ChatUiState(
val status: ConnectionStatus = ConnectionStatus.IDLE,
@ -89,7 +92,8 @@ data class ChatUiState(
val loadingPublicKey: Boolean = false,
val themeId: String = "blue",
val useDynamicColor: Boolean = true,
val language: String = "zh"
val language: String = "zh",
val notificationSound: String = "default"
) {
/**
* 是否允许连接
@ -112,7 +116,6 @@ data class ChatUiState(
val canSend: Boolean
get() = status == ConnectionStatus.READY && draft.trim().isNotEmpty() && !sending
/**
* 连接状态的简短文本描述
*/

@ -35,4 +35,5 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
fun updateTheme(themeId: String) = ChatSessionManager.updateTheme(themeId)
fun updateUseDynamicColor(enabled: Boolean) = ChatSessionManager.updateUseDynamicColor(enabled)
fun updateLanguage(language: String) = ChatSessionManager.updateLanguage(language)
fun updateNotificationSound(sound: String) = ChatSessionManager.updateNotificationSound(sound)
}

@ -2,7 +2,7 @@ package com.onlinemsg.client.util
/**
* 语言管理类统一存储应用内的多语言词条
* 类似于 Minecraft 语言文件映射
* 类似于 Minecraft language 文件映射
*/
object LanguageManager {
@ -32,6 +32,11 @@ object LanguageManager {
"settings.show_system" to "显示系统消息",
"settings.clear_msg" to "清空消息",
"settings.dynamic_color" to "使用动态颜色",
"settings.notification_sound" to "通知音效",
"sound.default" to "默认",
"sound.ding" to "",
"sound.nameit5" to "音效5",
"sound.wind_chime" to "风铃",
"chat.broadcast" to "广播",
"chat.private" to "私聊",
"chat.target_key" to "目标公钥",
@ -101,6 +106,11 @@ object LanguageManager {
"settings.show_system" to "Show System Messages",
"settings.clear_msg" to "ClearMsg",
"settings.dynamic_color" to "Use dynamic color",
"settings.notification_sound" to "Notification Sound",
"sound.default" to "Default",
"sound.ding" to "Ding",
"sound.nameit5" to "Sound 5",
"sound.wind_chime" to "Wind Chime",
"chat.broadcast" to "Broadcast",
"chat.private" to "Private",
"chat.target_key" to "Target Public Key",
@ -170,6 +180,11 @@ object LanguageManager {
"settings.show_system" to "システムメッセージを表示",
"settings.clear_msg" to "履歴を消去",
"settings.dynamic_color" to "動的カラーを使用",
"settings.notification_sound" to "通知音",
"sound.default" to "デフォルト",
"sound.ding" to "ディン",
"sound.nameit5" to "音効5",
"sound.wind_chime" to "風鈴",
"chat.broadcast" to "全体",
"chat.private" to "個人",
"chat.target_key" to "相手の公開鍵",
@ -239,6 +254,11 @@ object LanguageManager {
"settings.show_system" to "시스템 메시지 표시",
"settings.clear_msg" to "정보 삭제",
"settings.dynamic_color" to "동적 색상 사용",
"settings.notification_sound" to "알림 소리",
"sound.default" to "기본값",
"sound.ding" to "",
"sound.nameit5" to "효과음 5",
"sound.wind_chime" to "풍경",
"chat.broadcast" to "브로드캐스트",
"chat.private" to "비공개 채팅",
"chat.target_key" to "대상 공개키",
@ -309,6 +329,11 @@ object LanguageManager {
"settings.show_system" to "顯示系統訊息",
"settings.clear_msg" to "清空訊息",
"settings.dynamic_color" to "使用動態顏色",
"settings.notification_sound" to "通知音效",
"sound.default" to "預設",
"sound.ding" to "",
"sound.nameit5" to "音效5",
"sound.wind_chime" to "風鈴",
"chat.broadcast" to "廣播",
"chat.private" to "私訊",
"chat.target_key" to "目標公鑰",

Loading…
Cancel
Save