diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index cc4f7046e..e83f365cd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -325,6 +325,7 @@ interface SomeChat { val apiId: Long val ready: Boolean val sendMsgEnabled: Boolean + val ntfsEnabled: Boolean val createdAt: Instant val updatedAt: Instant } @@ -383,6 +384,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contact.apiId override val ready get() = contact.ready override val sendMsgEnabled get() = contact.sendMsgEnabled + override val ntfsEnabled get() = contact.chatSettings.enableNtfs override val createdAt get() = contact.createdAt override val updatedAt get() = contact.updatedAt override val displayName get() = contact.displayName @@ -402,6 +404,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready override val sendMsgEnabled get() = groupInfo.sendMsgEnabled + override val ntfsEnabled get() = groupInfo.chatSettings.enableNtfs override val createdAt get() = groupInfo.createdAt override val updatedAt get() = groupInfo.updatedAt override val displayName get() = groupInfo.displayName @@ -421,6 +424,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready override val sendMsgEnabled get() = contactRequest.sendMsgEnabled + override val ntfsEnabled get() = false override val createdAt get() = contactRequest.createdAt override val updatedAt get() = contactRequest.updatedAt override val displayName get() = contactRequest.displayName @@ -440,6 +444,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready override val sendMsgEnabled get() = contactConnection.sendMsgEnabled + override val ntfsEnabled get() = false override val createdAt get() = contactConnection.createdAt override val updatedAt get() = contactConnection.updatedAt override val displayName get() = contactConnection.displayName @@ -454,13 +459,13 @@ sealed class ChatInfo: SomeChat, NamedChat { } @Serializable -class Contact( +data class Contact( val contactId: Long, override val localDisplayName: String, val profile: Profile, val activeConn: Connection, val viaGroup: Long? = null, -// val chatSettings: ChatSettings, + val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { @@ -469,6 +474,7 @@ class Contact( override val apiId get() = contactId override val ready get() = activeConn.connStatus == ConnStatus.Ready override val sendMsgEnabled get() = true + override val ntfsEnabled get() = chatSettings.enableNtfs override val displayName get() = profile.displayName override val fullName get() = profile.fullName override val image get() = profile.image @@ -482,6 +488,7 @@ class Contact( localDisplayName = "alice", profile = Profile.sampleData, activeConn = Connection.sampleData, + chatSettings = ChatSettings(true), createdAt = Clock.System.now(), updatedAt = Clock.System.now() ) @@ -536,12 +543,12 @@ class Group ( ) @Serializable -class GroupInfo ( +data class GroupInfo ( val groupId: Long, override val localDisplayName: String, val groupProfile: GroupProfile, val membership: GroupMember, - // val chatSettings: ChatSettings, + val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { @@ -550,6 +557,7 @@ class GroupInfo ( override val apiId get() = groupId override val ready get() = true override val sendMsgEnabled get() = membership.memberActive + override val ntfsEnabled get() = chatSettings.enableNtfs override val displayName get() = groupProfile.displayName override val fullName get() = groupProfile.fullName override val image get() = groupProfile.image @@ -569,6 +577,7 @@ class GroupInfo ( localDisplayName = "team", groupProfile = GroupProfile.sampleData, membership = GroupMember.sampleData, + chatSettings = ChatSettings(true), createdAt = Clock.System.now(), updatedAt = Clock.System.now() ) @@ -770,6 +779,7 @@ class UserContactRequest ( override val apiId get() = contactRequestId override val ready get() = true override val sendMsgEnabled get() = false + override val ntfsEnabled get() = false override val displayName get() = profile.displayName override val fullName get() = profile.fullName override val image get() = profile.image @@ -799,6 +809,7 @@ class PendingContactConnection( override val apiId get() = pccConnId override val ready get() = false override val sendMsgEnabled get() = false + override val ntfsEnabled get() = false override val localDisplayName get() = String.format(generalGetString(R.string.connection_local_display_name), pccConnId) override val displayName: String get() { val initiated = pccConnStatus.initiated diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt index c64b1c8d2..c854183a7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -64,6 +64,8 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference } fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) { + if (!cInfo.ntfsEnabled) return + notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index a9684da5a..216688295 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -388,6 +388,17 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } + suspend fun apiSetSettings(type: ChatType,id: Long, settings: ChatSettings): Boolean { + val r = sendCmd(CC.APISetChatSettings(type, id, settings)) + return when (r) { + is CR.CmdOk -> true + else -> { + Log.e(TAG, "apiSetSettings bad response: ${r.responseType} ${r.details}") + false + } + } + } + suspend fun apiContactInfo(contactId: Long): ConnectionStats? { val r = sendCmd(CC.APIContactInfo(contactId)) if (r is CR.ContactInfo) return r.connectionStats @@ -942,7 +953,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } }, confirmButton = { - Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } + TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } } ) } @@ -973,7 +984,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } }, confirmButton = { - Button(onClick = ignoreOptimization) { Text(stringResource(R.string.ok)) } + TextButton(onClick = ignoreOptimization) { Text(stringResource(R.string.ok)) } } ) } @@ -999,7 +1010,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } }, confirmButton = { - Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } + TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } } ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index e214fdcda..72c656261 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -43,11 +43,43 @@ fun ChatInfoView(chatModel: ChatModel, connStats: ConnectionStats?, close: () -> connStats, developerTools, deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) }, - clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) } + clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, + changeNtfsState = { enabled -> + changeNtfsState(enabled, chat, chatModel) + }, ) } } +fun changeNtfsState(enabled: Boolean, chat: Chat, chatModel: ChatModel) { + val newChatInfo = when(chat.chatInfo) { + is ChatInfo.Direct -> with (chat.chatInfo) { + ChatInfo.Direct(contact.copy(chatSettings = contact.chatSettings.copy(enableNtfs = enabled))) + } + is ChatInfo.Group -> with(chat.chatInfo) { + ChatInfo.Group(groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(enableNtfs = enabled))) + } + else -> null + } + withApi { + val res = when (newChatInfo) { + is ChatInfo.Direct -> with(newChatInfo) { + chatModel.controller.apiSetSettings(chatType, apiId, contact.chatSettings) + } + is ChatInfo.Group -> with(newChatInfo) { + chatModel.controller.apiSetSettings(chatType, apiId, groupInfo.chatSettings) + } + else -> false + } + if (res && newChatInfo != null) { + chatModel.updateChatInfo(newChatInfo) + if (!enabled) { + chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) + } + } + } +} + fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { AlertManager.shared.showAlertMsg( title = generalGetString(R.string.delete_contact_question), @@ -91,7 +123,8 @@ fun ChatInfoLayout( connStats: ConnectionStats?, developerTools: Boolean, deleteContact: () -> Unit, - clearChat: () -> Unit + clearChat: () -> Unit, + changeNtfsState: (Boolean) -> Unit, ) { Column( Modifier @@ -126,6 +159,17 @@ fun ChatInfoLayout( SectionSpacer() } + var ntfsEnabled by remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + SectionView(title = stringResource(R.string.settings_section_title_settings)) { + SectionItemView { + NtfsSwitch(ntfsEnabled) { + ntfsEnabled = !ntfsEnabled + changeNtfsState(ntfsEnabled) + } + } + } + SectionSpacer() + SectionView { SectionItemView { ClearChatButton(clearChat) @@ -236,6 +280,38 @@ fun SimplexServers(text: String, servers: List) { } } +@Composable +fun NtfsSwitch( + ntfsEnabled: Boolean, + toggleNtfs: (Boolean) -> Unit +) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Outlined.Notifications, + stringResource(R.string.notifications), + tint = HighOrLowlight + ) + Text(stringResource(R.string.notifications)) + } + Switch( + checked = ntfsEnabled, + onCheckedChange = toggleNtfs, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ), + ) + } +} + @Composable fun ClearChatButton(clearChat: () -> Unit) { Row( @@ -282,6 +358,7 @@ fun PreviewChatInfoLayout() { chatItems = arrayListOf(), serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT")) ), + changeNtfsState = {}, developerTools = false, connStats = null, deleteContact = {}, clearChat = {} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index 4ebc59d5b..d2d778771 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -72,7 +72,10 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { }, deleteGroup = { deleteGroupDialog(chat.chatInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, - leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) } + leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) }, + changeNtfsState = { enabled -> + changeNtfsState(enabled, chat, chatModel) + }, ) } } @@ -122,6 +125,7 @@ fun GroupChatInfoLayout( deleteGroup: () -> Unit, clearChat: () -> Unit, leaveGroup: () -> Unit, + changeNtfsState: (Boolean) -> Unit, ) { Column( Modifier @@ -154,6 +158,17 @@ fun GroupChatInfoLayout( } SectionSpacer() + var ntfsEnabled by remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + SectionView(title = stringResource(R.string.settings_section_title_settings)) { + SectionItemView { + NtfsSwitch(ntfsEnabled) { + ntfsEnabled = !ntfsEnabled + changeNtfsState(ntfsEnabled) + } + } + } + SectionSpacer() + SectionView { if (groupInfo.canEdit) { SectionItemView { @@ -322,7 +337,8 @@ fun PreviewGroupChatInfoLayout() { groupInfo = GroupInfo.sampleData, members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, - addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {} + addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, + changeNtfsState = {}, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 7392526c3..9c8f05238 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -184,13 +184,13 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, deleteMessage: (Long, CIDeleteM .padding(horizontal = 8.dp, vertical = 2.dp), horizontalArrangement = Arrangement.End, ) { - Button(onClick = { + TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmInternal) AlertManager.shared.hideAlert() }) { Text(stringResource(R.string.for_me_only)) } if (chatItem.meta.editable) { Spacer(Modifier.padding(horizontal = 4.dp)) - Button(onClick = { + TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast) AlertManager.shared.hideAlert() }) { Text(stringResource(R.string.for_everybody)) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index ade4c9abe..fb9faf545 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -119,6 +119,7 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { + ItemAction( + if (ntfsEnabled) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat), + if (ntfsEnabled) Icons.Outlined.NotificationsOff else Icons.Outlined.Notifications, + onClick = { + changeNtfsState(!ntfsEnabled, chat, chatModel) + showMenu.value = false + } + ) +} + @Composable fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { ItemAction( @@ -318,14 +332,14 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel .padding(horizontal = 8.dp, vertical = 2.dp), horizontalArrangement = Arrangement.End, ) { - Button(onClick = { + TextButton(onClick = { AlertManager.shared.hideAlert() deleteContactConnectionAlert(connection, chatModel) }) { Text(stringResource(R.string.delete_verb)) } Spacer(Modifier.padding(horizontal = 4.dp)) - Button(onClick = { AlertManager.shared.hideAlert() }) { + TextButton(onClick = { AlertManager.shared.hideAlert() }) { Text(stringResource(R.string.ok)) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 65161deee..5bb8e8526 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -8,7 +8,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,8 +24,7 @@ import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.item.MarkdownText -import chat.simplex.app.views.helpers.ChatInfoImage -import chat.simplex.app.views.helpers.badgeLayout +import chat.simplex.app.views.helpers.* @Composable fun ChatPreviewView(chat: Chat, stopped: Boolean) { @@ -134,6 +134,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { modifier = Modifier.padding(bottom = 5.dp) ) val n = chat.chatStats.unreadCount + val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) if (n > 0) { Box( Modifier.padding(top = 24.dp), @@ -144,12 +145,27 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) { color = MaterialTheme.colors.onPrimary, fontSize = 11.sp, modifier = Modifier - .background(if (stopped) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape) + .background(if (stopped || showNtfsIcon) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape) .badgeLayout() .padding(horizontal = 3.dp) .padding(vertical = 1.dp) ) } + } else if (showNtfsIcon) { + Box( + Modifier.padding(top = 24.dp), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Filled.NotificationsOff, + contentDescription = generalGetString(R.string.notifications), + tint = HighOrLowlight, + modifier = Modifier + .padding(horizontal = 3.dp) + .padding(vertical = 1.dp) + .size(17.dp) + ) + } } if (cInfo is ChatInfo.Direct) { Box( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt index 2adaf878e..55715f8e8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt @@ -53,13 +53,13 @@ class AlertManager { title = { Text(title) }, text = alertText, confirmButton = { - Button(onClick = { + TextButton(onClick = { onConfirm?.invoke() hideAlert() }) { Text(confirmText) } }, dismissButton = { - Button(onClick = { + TextButton(onClick = { onDismiss?.invoke() hideAlert() }) { Text(dismissText) } diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 6a830e114..b8246adb7 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -141,6 +141,9 @@ Файл не найден Ошибка сохранения файла + + Уведомления + Удалить контакт? Контакт и все сообщения будут удалены - это действие нельзя отменить! @@ -205,6 +208,10 @@ Очистить чат Прочитано + + Без звука + Уведомлять + Вы пригласили ваш контакт Вы приняли приглашение соединиться diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index fd4f5a519..ca4aeb42e 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -141,6 +141,9 @@ File not found Error saving file + + Notifications + Delete contact? Contact and all messages will be deleted - this cannot be undone! @@ -205,6 +208,10 @@ Clear chat Mark read + + Mute + Unmute + You invited your contact You accepted connection