From 8738cf332f6916bdc9cece0f13e6bc9de452a14c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:37:29 +0400 Subject: [PATCH 01/29] ui: fix chat preview showing incorrect timestamp when chat is empty (#3739) --- .../Views/ChatList/ChatPreviewView.swift | 2 +- apps/ios/SimpleXChat/ChatTypes.swift | 17 ++++++++++++- .../chat/simplex/common/model/ChatModel.kt | 24 +++++++++++++++---- .../common/views/chatlist/ChatPreviewView.kt | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 186a709ce8..3ad918b982 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -34,7 +34,7 @@ struct ChatPreviewView: View { HStack(alignment: .top) { chatPreviewTitle() Spacer() - (cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt)) + (cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs)) .font(.subheadline) .frame(minWidth: 60, alignment: .trailing) .foregroundColor(.secondary) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index ff61a51d3e..198a777f8b 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1367,6 +1367,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var chatTs: Date { + switch self { + case let .direct(contact): return contact.chatTs ?? contact.updatedAt + case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt + case let .local(noteFolder): return noteFolder.chatTs + case let .contactRequest(contactRequest): return contactRequest.updatedAt + case let .contactConnection(contactConnection): return contactConnection.updatedAt + case .invalidJSON: return .now + } + } + public struct SampleData { public var direct: ChatInfo public var group: ChatInfo @@ -1425,6 +1436,7 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var mergedPreferences: ContactUserPreferences var createdAt: Date var updatedAt: Date + var chatTs: Date? var contactGroupMemberId: Int64? var contactGrpInvSent: Bool @@ -1744,6 +1756,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date + var chatTs: Date? public var id: ChatId { get { "#\(groupId)" } } public var apiId: Int64 { get { groupId } } @@ -2049,6 +2062,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat { public var unread: Bool var createdAt: Date public var updatedAt: Date + var chatTs: Date public var id: ChatId { get { "*\(noteFolderId)" } } public var apiId: Int64 { get { noteFolderId } } @@ -2070,7 +2084,8 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat { favorite: false, unread: false, createdAt: .now, - updatedAt: .now + updatedAt: .now, + chatTs: .now ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index d44c80e92b..b68d098f91 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -969,6 +969,16 @@ sealed class ChatInfo: SomeChat, NamedChat { is Group -> groupInfo.chatSettings else -> null } + + val chatTs: Instant + get() = when(this) { + is Direct -> contact.chatTs ?: contact.updatedAt + is Group -> groupInfo.chatTs ?: groupInfo.updatedAt + is Local -> noteFolder.chatTs + is ContactRequest -> contactRequest.updatedAt + is ContactConnection -> contactConnection.updatedAt + is InvalidJSON -> updatedAt + } } @Serializable @@ -1009,6 +1019,7 @@ data class Contact( val mergedPreferences: ContactUserPreferences, override val createdAt: Instant, override val updatedAt: Instant, + val chatTs: Instant?, val contactGroupMemberId: Long? = null, val contactGrpInvSent: Boolean ): SomeChat, NamedChat { @@ -1077,6 +1088,7 @@ data class Contact( mergedPreferences = ContactUserPreferences.sampleData, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), + chatTs = Clock.System.now(), contactGrpInvSent = false ) } @@ -1204,7 +1216,8 @@ data class GroupInfo ( val hostConnCustomUserProfileId: Long? = null, val chatSettings: ChatSettings, override val createdAt: Instant, - override val updatedAt: Instant + override val updatedAt: Instant, + val chatTs: Instant? ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" @@ -1245,7 +1258,8 @@ data class GroupInfo ( hostConnCustomUserProfileId = null, chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false), createdAt = Clock.System.now(), - updatedAt = Clock.System.now() + updatedAt = Clock.System.now(), + chatTs = Clock.System.now() ) } } @@ -1507,7 +1521,8 @@ class NoteFolder( val favorite: Boolean, val unread: Boolean, override val createdAt: Instant, - override val updatedAt: Instant + override val updatedAt: Instant, + val chatTs: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.Local override val id get() = "*$noteFolderId" @@ -1530,7 +1545,8 @@ class NoteFolder( favorite = false, unread = false, createdAt = Clock.System.now(), - updatedAt = Clock.System.now() + updatedAt = Clock.System.now(), + chatTs = Clock.System.now() ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 08e95f391a..e17ae6ea7a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -286,7 +286,7 @@ fun ChatPreviewView( Box( contentAlignment = Alignment.TopEnd ) { - val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt) + val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs) Text( ts, color = MaterialTheme.colors.secondary, From f1ff27218c0fd765d9e445e27c39418feba12502 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:43:18 +0400 Subject: [PATCH 02/29] ui: align call buttons with calls preference (#3740) --- apps/ios/Shared/Views/Chat/ChatView.swift | 5 +++-- .../kotlin/chat/simplex/common/views/chat/ChatView.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index af53e7e476..0915d62873 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -159,12 +159,13 @@ struct ChatView: View { switch cInfo { case let .direct(contact): HStack { - if contact.allowsFeature(.calls) { + let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser + if callsPrefEnabled { callButton(contact, .audio, imageName: "phone") .disabled(!contact.ready || !contact.active) } Menu { - if contact.allowsFeature(.calls) { + if callsPrefEnabled { Button { CallController.shared.startCall(contact, .video) } label: { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 2e99d791b5..c01344164f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -647,7 +647,7 @@ fun ChatInfoToolbar( } } - if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) { + if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) { if (activeCall == null) { barButtons.add { if (appPlatform.isAndroid) { From 838a759a76dd055aeba2ae29ee606620ae4322d4 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:44:29 +0400 Subject: [PATCH 03/29] ui: deleted item preview (#3726) --- .../Views/Chat/ChatItem/MarkedDeletedItemView.swift | 2 ++ .../ios/Shared/Views/ChatList/ChatPreviewView.swift | 13 ++++++++++++- .../common/views/chat/item/MarkedDeletedItemView.kt | 2 +- .../common/views/chatlist/ChatPreviewView.kt | 3 ++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index dfa4a97fc2..cb0b61f537 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -65,6 +65,8 @@ struct MarkedDeletedItemView: View { } } + // same texts are in markedDeletedText in ChatPreviewView, but it returns String; + // can be refactored into a single function if functions calling these are changed to return same type var markedDeletedText: LocalizedStringKey { switch chatItem.meta.itemDeleted { case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)" diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 3ad918b982..8bfc8fec03 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -171,10 +171,21 @@ struct ChatPreviewView: View { } func chatItemPreview(_ cItem: ChatItem) -> Text { - let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text") + let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText() let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false) + // same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey; + // can be refactored into a single function if functions calling these are changed to return same type + func markedDeletedText() -> String { + switch cItem.meta.itemDeleted { + case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName) + case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text") + case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text") + case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text") + } + } + func attachment() -> String? { switch cItem.content.msgContent { case .file: return "doc.fill" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index f7783d682a..0e2e8867cb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -91,7 +91,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index e17ae6ea7a..1bb5a78996 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.chat.item.markedDeletedText import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -170,7 +171,7 @@ fun ChatPreviewView( val (text: CharSequence, inlineTextContent) = when { chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) } ci.meta.itemDeleted == null -> ci.text to null - else -> generalGetString(MR.strings.marked_deleted_description) to null + else -> markedDeletedText(ci.meta) to null } val formattedText = when { chatModelDraftChatId == chat.id && chatModelDraft != null -> null From da9a7f4642e550e1e5ebf902dd1ba8a60944d85c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:56:38 +0400 Subject: [PATCH 04/29] ui: exclude not ready and active contacts from list of contacts to add to group (e.g. simplex team contact) (#3737) --- apps/ios/Shared/Model/SimpleXAPI.swift | 2 +- .../chat/simplex/common/views/chat/group/AddGroupMembersView.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 24a77cb3d3..d1a16f73a8 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1172,7 +1172,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] { let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil } return ChatModel.shared.chats .compactMap{ $0.chatInfo.contact } - .filter{ !memberContactIds.contains($0.apiId) } + .filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) } .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index e4f31748cc..dcf3b36a5b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -86,7 +86,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List { .map { it.chatInfo } .filterIsInstance() .map { it.contact } - .filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) } + .filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) } .sortedBy { it.displayName.lowercase() } .toList() } From bd30b80e15a41d1cca894301b1a44cfc3da21c31 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 24 Jan 2024 23:15:27 +0700 Subject: [PATCH 05/29] desktop: custom time picker (#3741) * desktop: custom time picker * text color * formatting * changes in UI * optimization * desktop: opening SimpleX links inside the app (#3738) * 5.5: ios 194, android 175, desktop 26 * docs: update downloads page * ui: fix chat preview showing incorrect timestamp when chat is empty (#3739) * ui: align call buttons with calls preference (#3740) * ui: deleted item preview (#3726) --------- Co-authored-by: Evgeny Poberezkin Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- .../views/helpers/CustomTimePicker.android.kt | 106 +++++++++ .../simplex/common/views/chat/SendMsgView.kt | 127 ++++------- .../common/views/chat/item/ChatItemView.kt | 17 ++ .../common/views/helpers/CustomTimePicker.kt | 204 ++++-------------- .../views/helpers/CustomTimePicker.desktop.kt | 80 +++++++ 5 files changed, 277 insertions(+), 257 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt new file mode 100644 index 0000000000..18f3455e36 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt @@ -0,0 +1,106 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.CustomTimeUnit +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import com.sd.lib.compose.wheel_picker.* + +@Composable +actual fun CustomTimePicker( + selection: MutableState, + timeUnitsLimits: List +) { + fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List { + val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit) + val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList() + return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue) + } + + val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value) + val selectedUnit: MutableState = remember { mutableStateOf(unit) } + val selectedDuration = remember { mutableStateOf(duration) } + val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) } + val isTriggered = remember { mutableStateOf(false) } + + LaunchedEffect(selectedUnit.value) { + // on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue + // (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120), + // selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition + if (isTriggered.value) { + val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue + if (maxValue != null && selectedDuration.value > maxValue) { + selectedDuration.value = maxValue + selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) + } else { + selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) + selection.value = selectedUnit.value.toSeconds * selectedDuration.value + } + } else { + isTriggered.value = true + } + } + + LaunchedEffect(selectedDuration.value) { + selection.value = selectedUnit.value.toSeconds * selectedDuration.value + } + + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.spacedBy(0.dp) + ) { + Column(Modifier.weight(1f)) { + val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value)) + FVerticalWheelPicker( + count = selectedUnitValues.value.count(), + state = durationPickerState, + unfocusedCount = 2, + focus = { + FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) + } + ) { index -> + Text( + selectedUnitValues.value[index].toString(), + fontSize = 18.sp, + color = MaterialTheme.colors.primary + ) + } + LaunchedEffect(durationPickerState) { + snapshotFlow { durationPickerState.currentIndex } + .collect { + selectedDuration.value = selectedUnitValues.value[it] + } + } + } + Column(Modifier.weight(1f)) { + val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value }) + FVerticalWheelPicker( + count = timeUnitsLimits.count(), + state = unitPickerState, + unfocusedCount = 2, + focus = { + FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) + } + ) { index -> + Text( + timeUnitsLimits[index].timeUnit.text, + fontSize = 18.sp, + color = MaterialTheme.colors.primary + ) + } + LaunchedEffect(unitPickerState) { + snapshotFlow { unitPickerState.currentIndex } + .collect { + selectedUnit.value = timeUnitsLimits[it].timeUnit + } + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index f1079d2f5f..456e2a538b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -59,14 +59,6 @@ fun SendMsgView( ) { val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } - if (showCustomDisappearingMessageDialog.value) { - CustomDisappearingMessageDialog( - sendMessage = sendMessage, - setShowDialog = { showCustomDisappearingMessageDialog.value = it }, - customDisappearingMessageTimePref = customDisappearingMessageTimePref - ) - } - Box(Modifier.padding(vertical = 8.dp)) { val cs = composeState.value var progressByTimeout by rememberSaveable { mutableStateOf(false) } @@ -203,6 +195,11 @@ fun SendMsgView( DefaultDropdownMenu(showDropdown) { menuItems.forEach { composable -> composable() } } + CustomDisappearingMessageDialog( + showCustomDisappearingMessageDialog, + sendMessage = sendMessage, + customDisappearingMessageTimePref = customDisappearingMessageTimePref + ) } else { SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) } @@ -220,93 +217,43 @@ expect fun VoiceButtonWithoutPermissionByPlatform() @Composable private fun CustomDisappearingMessageDialog( + showMenu: MutableState, sendMessage: (Int?) -> Unit, - setShowDialog: (Boolean) -> Unit, customDisappearingMessageTimePref: SharedPreference? ) { - val showCustomTimePicker = remember { mutableStateOf(false) } - - if (showCustomTimePicker.value) { - val selectedDisappearingMessageTime = remember { - mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300) - } - CustomTimePickerDialog( - selectedDisappearingMessageTime, - title = generalGetString(MR.strings.delete_after), - confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send), - confirmButtonAction = { ttl -> - sendMessage(ttl) - customDisappearingMessageTimePref?.set?.invoke(ttl) - setShowDialog(false) - }, - cancel = { setShowDialog(false) } + DefaultDropdownMenu(showMenu) { + Text( + generalGetString(MR.strings.send_disappearing_message), + Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + color = MaterialTheme.colors.secondary ) - } else { - @Composable - fun ChoiceButton( - text: String, - onClick: () -> Unit - ) { - TextButton(onClick) { - Text( - text, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - } - DefaultDialog(onDismissRequest = { setShowDialog(false) }) { - Surface( - shape = RoundedCornerShape(corner = CornerSize(25.dp)), - contentColor = LocalContentColor.current - ) { - Box( - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier.padding(DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(6.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(" ") // centers title - Text( - generalGetString(MR.strings.send_disappearing_message), - fontSize = 16.sp, - color = MaterialTheme.colors.secondary - ) - Icon( - painterResource(MR.images.ic_close), - generalGetString(MR.strings.icon_descr_close_button), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(25.dp) - .clickable { setShowDialog(false) } - ) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) { - sendMessage(30) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) { - sendMessage(60) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) { - sendMessage(300) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) { - showCustomTimePicker.value = true - } - } - } - } + ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) { + sendMessage(30) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) { + sendMessage(60) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) { + sendMessage(300) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) { + showMenu.value = false + val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300) + showCustomTimePickerDialog( + selectedDisappearingMessageTime, + title = generalGetString(MR.strings.delete_after), + confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send), + confirmButtonAction = { ttl -> + sendMessage(ttl) + customDisappearingMessageTimePref?.set?.invoke(ttl) + }, + cancel = { showMenu.value = false } + ) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 549f5f2f52..e0f31f65c2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -651,6 +651,23 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo } } +@Composable +fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Unit) { + val finalColor = if (color == Color.Unspecified) { + MenuTextColor + } else color + DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { + Text( + text, + modifier = Modifier + .fillMaxWidth() + .weight(1F) + .padding(end = 15.dp), + color = finalColor + ) + } +} + fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) { AlertManager.shared.showAlertDialog( title = generalGetString(cancelAction.alert.titleId), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt index f13edd618d..3c44cbb4dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt @@ -1,116 +1,21 @@ package chat.simplex.common.views.helpers -import androidx.compose.foundation.clickable +import SectionItemView import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import dev.icerock.moko.resources.compose.painterResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import chat.simplex.common.ui.theme.DEFAULT_PADDING +import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.CustomTimeUnit import chat.simplex.common.model.timeText import chat.simplex.res.MR -import com.sd.lib.compose.wheel_picker.* @Composable -fun CustomTimePicker( +expect fun CustomTimePicker( selection: MutableState, timeUnitsLimits: List = TimeUnitLimits.defaultUnitsLimits -) { - fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List { - val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit) - val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList() - return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue) - } - - val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value) - val selectedUnit: MutableState = remember { mutableStateOf(unit) } - val selectedDuration = remember { mutableStateOf(duration) } - val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) } - val isTriggered = remember { mutableStateOf(false) } - - LaunchedEffect(selectedUnit.value) { - // on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue - // (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120), - // selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition - if (isTriggered.value) { - val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue - if (maxValue != null && selectedDuration.value > maxValue) { - selectedDuration.value = maxValue - selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) - } else { - selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) - selection.value = selectedUnit.value.toSeconds * selectedDuration.value - } - } else { - isTriggered.value = true - } - } - - LaunchedEffect(selectedDuration.value) { - selection.value = selectedUnit.value.toSeconds * selectedDuration.value - } - - Row( - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING), - horizontalArrangement = Arrangement.spacedBy(0.dp) - ) { - Column(Modifier.weight(1f)) { - val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value)) - FVerticalWheelPicker( - count = selectedUnitValues.value.count(), - state = durationPickerState, - unfocusedCount = 2, - focus = { - FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) - } - ) { index -> - Text( - selectedUnitValues.value[index].toString(), - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - LaunchedEffect(durationPickerState) { - snapshotFlow { durationPickerState.currentIndex } - .collect { - selectedDuration.value = selectedUnitValues.value[it] - } - } - } - Column(Modifier.weight(1f)) { - val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value }) - FVerticalWheelPicker( - count = timeUnitsLimits.count(), - state = unitPickerState, - unfocusedCount = 2, - focus = { - FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) - } - ) { index -> - Text( - timeUnitsLimits[index].timeUnit.text, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - LaunchedEffect(unitPickerState) { - snapshotFlow { unitPickerState.currentIndex } - .collect { - selectedUnit.value = timeUnitsLimits[it].timeUnit - } - } - } - } -} +) data class TimeUnitLimits( val timeUnit: CustomTimeUnit, @@ -141,8 +46,7 @@ data class TimeUnitLimits( } } -@Composable -fun CustomTimePickerDialog( +fun showCustomTimePickerDialog( selection: MutableState, timeUnitsLimits: List = TimeUnitLimits.defaultUnitsLimits, title: String, @@ -150,53 +54,26 @@ fun CustomTimePickerDialog( confirmButtonAction: (Int) -> Unit, cancel: () -> Unit ) { - DefaultDialog(onDismissRequest = cancel) { - Surface( - shape = RoundedCornerShape(corner = CornerSize(25.dp)), - contentColor = LocalContentColor.current - ) { - Box( - contentAlignment = Alignment.Center + AlertManager.shared.showAlertDialogButtonsColumn( + title = title, + onDismissRequest = cancel + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CustomTimePicker( + selection, + timeUnitsLimits + ) + SectionItemView({ + AlertManager.shared.hideAlert() + confirmButtonAction(selection.value) + } ) { - Column( - modifier = Modifier.padding(DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(6.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(" ") // centers title - Text( - title, - fontSize = 16.sp, - color = MaterialTheme.colors.secondary - ) - Icon( - painterResource(MR.images.ic_close), - generalGetString(MR.strings.icon_descr_close_button), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(25.dp) - .clickable { cancel() } - ) - } - - CustomTimePicker( - selection, - timeUnitsLimits - ) - - TextButton(onClick = { confirmButtonAction(selection.value) }) { - Text( - confirmButtonText, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - } + Text( + confirmButtonText, + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) } } } @@ -220,7 +97,6 @@ fun DropdownCustomTimePickerSettingRow( val dropdownSelection: MutableState = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) } val values: MutableState> = remember { mutableStateOf(getValues(selection.value)) } - val showCustomTimePicker = remember { mutableStateOf(false) } fun updateValue(selectedValue: Int?) { values.value = getValues(selectedValue) @@ -247,28 +123,22 @@ fun DropdownCustomTimePickerSettingRow( onSelected = { sel: DropdownSelection -> when (sel) { is DropdownSelection.DropdownValue -> updateValue(sel.value) - DropdownSelection.Custom -> showCustomTimePicker.value = true + DropdownSelection.Custom -> { + val selectedCustomTime = mutableStateOf(selection.value ?: 86400) + showCustomTimePickerDialog( + selectedCustomTime, + timeUnitsLimits = customPickerTimeUnitsLimits, + title = customPickerTitle, + confirmButtonText = customPickerConfirmButtonText, + confirmButtonAction = ::updateValue, + cancel = { + dropdownSelection.value = DropdownSelection.DropdownValue(selection.value) + } + ) + } } } ) - - if (showCustomTimePicker.value) { - val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) } - CustomTimePickerDialog( - selectedCustomTime, - timeUnitsLimits = customPickerTimeUnitsLimits, - title = customPickerTitle, - confirmButtonText = customPickerConfirmButtonText, - confirmButtonAction = { time -> - updateValue(time) - showCustomTimePicker.value = false - }, - cancel = { - dropdownSelection.value = DropdownSelection.DropdownValue(selection.value) - showCustomTimePicker.value = false - } - ) - } } private sealed class DropdownSelection { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt new file mode 100644 index 0000000000..03c8e51c55 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt @@ -0,0 +1,80 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import chat.simplex.common.model.CustomTimeUnit +import chat.simplex.common.ui.theme.DEFAULT_PADDING + +@Composable +actual fun CustomTimePicker( + selection: MutableState, + timeUnitsLimits: List +) { + val unit = remember { + var res: CustomTimeUnit = CustomTimeUnit.Second + val found = timeUnitsLimits.asReversed().any { + if (selection.value >= it.minValue * it.timeUnit.toSeconds && selection.value <= it.maxValue * it.timeUnit.toSeconds) { + res = it.timeUnit + selection.value = (selection.value / it.timeUnit.toSeconds).coerceIn(it.minValue, it.maxValue) * it.timeUnit.toSeconds + true + } else { + false + } + } + if (!found) { + // If custom interval doesn't fit in any category, set it to 1 second interval + selection.value = 1 + } + mutableStateOf(res) + } + val values = remember(unit.value) { + val limit = timeUnitsLimits.first { it.timeUnit == unit.value } + val res = ArrayList>() + for (i in limit.minValue..limit.maxValue) { + val seconds = i * limit.timeUnit.toSeconds + val desc = i.toString() + res.add(seconds to desc) + } + if (res.none { it.first == selection.value }) { + // Doesn't fit into min..max, put it equal to the closest value + selection.value = selection.value.coerceIn(res.first().first, res.last().first) + //selection.value = res.last { it.first <= selection.value }.first + } + res + } + val units = remember { + val res = ArrayList>() + for (unit in timeUnitsLimits) { + res.add(unit.timeUnit to unit.timeUnit.text) + } + res + } + + Row( + Modifier.padding(bottom = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ExposedDropDownSetting( + values, + selection, + textColor = MaterialTheme.colors.onBackground, + enabled = remember { mutableStateOf(true) }, + onSelected = { selection.value = it } + ) + Spacer(Modifier.width(DEFAULT_PADDING)) + ExposedDropDownSetting( + units, + unit, + textColor = MaterialTheme.colors.onBackground, + enabled = remember { mutableStateOf(true) }, + onSelected = { + selection.value = selection.value / unit.value.toSeconds * it.toSeconds + unit.value = it + } + ) + } +} From f81e457e0955498e45c5d6c957d975d762d922f2 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 24 Jan 2024 23:22:29 +0700 Subject: [PATCH 06/29] android: trying to start service again in case it was destroyed (#3745) --- .../src/main/java/chat/simplex/app/SimplexApp.kt | 12 ++++++++++-- .../src/main/java/chat/simplex/app/SimplexService.kt | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index f9c2eac134..a99cb11488 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -197,10 +197,18 @@ class SimplexApp: Application(), LifecycleEventObserver { } SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) CoroutineScope(Dispatchers.Default).launch { - if (mode == NotificationsMode.SERVICE) + if (mode == NotificationsMode.SERVICE) { SimplexService.start() - else + // Sometimes, when we change modes fast from one to another, system destroys the service after start. + // We can wait a little and restart the service, and it will work in 100% of cases + delay(2000) + if (!SimplexService.isServiceStarted && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE) { + Log.i(TAG, "Service tried to start but destroyed by system, repeating once more") + SimplexService.start() + } + } else { SimplexService.safeStopService() + } } if (mode != NotificationsMode.PERIODIC) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index dd760e0b19..c58959d5cc 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -262,7 +262,7 @@ class SimplexService: Service() { private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE" private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce" - private var isServiceStarted = false + var isServiceStarted = false private var stopAfterStart = false fun scheduleStart(context: Context) { From afc324dc4f4433fd36920aceb049758e501b07b7 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 24 Jan 2024 23:24:49 +0700 Subject: [PATCH 07/29] android, desktop: marking chat as read if it was set unread (#3746) --- .../chat/simplex/common/views/chat/ChatView.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index c01344164f..6c516009fd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -68,12 +68,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() .onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") } - .filter { it != null && activeChat.value?.id != it } + .filterNotNull() .collect { chatId -> - // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly - // Also for situation when chatId changes after clicking in notification, etc - activeChat.value = chatModel.getChat(chatId!!) - Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}") + if (activeChat.value?.id != chatId) { + // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly + // Also for situation when chatId changes after clicking in notification, etc + activeChat.value = chatModel.getChat(chatId) + Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}") + } markUnreadChatAsRead(activeChat, chatModel) } } From da1d20c17fc0392d8bcc6a68f46d31b9dea7c9ed Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 24 Jan 2024 23:25:44 +0700 Subject: [PATCH 08/29] desktop: alignment for reactions (#3747) --- .../kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index e0f31f65c2..93d5430a26 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -103,7 +103,7 @@ fun ChatItemView( setReaction(cInfo, cItem, !r.userReacted, r.reaction) } } - Row(modifier.padding(2.dp)) { + Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) { ReactionIcon(r.reaction.text, fontSize = 12.sp) if (r.totalReacted > 1) { Spacer(Modifier.width(4.dp)) @@ -112,7 +112,6 @@ fun ChatItemView( fontSize = 11.5.sp, fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal, color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - modifier = if (appPlatform.isAndroid) Modifier else Modifier.padding(top = 4.dp) ) } } From fbe33534342e18344ca399651a999b5c6c12c26d Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:58:39 +0400 Subject: [PATCH 09/29] ui: fix link preview cancellation (#3750) --- apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift | 3 +++ .../kotlin/chat/simplex/common/views/chat/ComposeView.kt | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index b597926093..604e0a276d 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -978,6 +978,9 @@ struct ComposeView: View { } private func cancelLinkPreview() { + if let pendingLink = pendingLinkUrl?.absoluteString { + cancelledLinks.insert(pendingLink) + } if let uri = composeState.linkPreview?.uri.absoluteString { cancelledLinks.insert(uri) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index e5982d01db..539c59188b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -583,6 +583,10 @@ fun ComposeView( } fun cancelLinkPreview() { + val pendingLink = pendingLinkUrl.value + if (pendingLink != null) { + cancelledLinks.add(pendingLink) + } val uri = composeState.value.linkPreview?.uri if (uri != null) { cancelledLinks.add(uri) From 6ef3a9e66886b9ec892bcca557dd2a7eb469130d Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:08:10 +0400 Subject: [PATCH 10/29] ios: fix welcome view (#3743) * welcome view (still doesn't keep change on re-open) * fix * remove debug log * rename --- .../Views/Chat/Group/GroupChatInfoView.swift | 6 +++- .../Views/Chat/Group/GroupWelcomeView.swift | 33 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 3879e78d3d..dbea6a17e0 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -370,7 +370,11 @@ struct GroupChatInfoView: View { private func addOrEditWelcomeMessage() -> some View { NavigationLink { - GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo) + GroupWelcomeView( + groupInfo: $groupInfo, + groupProfile: groupInfo.groupProfile, + welcomeText: groupInfo.groupProfile.description ?? "" + ) .navigationTitle("Welcome message") .navigationBarTitleDisplayMode(.large) } label: { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index e5ff644a91c..c69cc526d4 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -11,10 +11,9 @@ import SimpleXChat struct GroupWelcomeView: View { @Environment(\.dismiss) var dismiss: DismissAction - @EnvironmentObject private var m: ChatModel - var groupId: Int64 @Binding var groupInfo: GroupInfo - @State private var welcomeText: String = "" + @State var groupProfile: GroupProfile + @State var welcomeText: String @State private var editMode = true @FocusState private var keyboardVisible: Bool @State private var showSaveDialog = false @@ -24,7 +23,7 @@ struct GroupWelcomeView: View { if groupInfo.canEdit { editorView() .modifier(BackButton { - if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) { + if welcomeTextUnchanged() { dismiss() } else { showSaveDialog = true @@ -33,7 +32,6 @@ struct GroupWelcomeView: View { .confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) { Button("Save and update group profile") { save() - dismiss() } Button("Exit without saving") { dismiss() } } @@ -47,8 +45,9 @@ struct GroupWelcomeView: View { } } .onAppear { - welcomeText = groupInfo.groupProfile.description ?? "" - keyboardVisible = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + keyboardVisible = true + } } } @@ -113,7 +112,11 @@ struct GroupWelcomeView: View { Button("Save and update group profile") { save() } - .disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)) + .disabled(welcomeTextUnchanged()) + } + + private func welcomeTextUnchanged() -> Bool { + welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) } private func save() { @@ -123,11 +126,13 @@ struct GroupWelcomeView: View { if welcome?.count == 0 { welcome = nil } - var groupProfileUpdated = groupInfo.groupProfile - groupProfileUpdated.description = welcome - groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated) - m.updateGroup(groupInfo) - welcomeText = welcome ?? "" + groupProfile.description = welcome + let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile) + await MainActor.run { + groupInfo = gInfo + ChatModel.shared.updateGroup(gInfo) + dismiss() + } } catch let error { logger.error("apiUpdateGroup error: \(responseError(error))") } @@ -137,6 +142,6 @@ struct GroupWelcomeView: View { struct GroupWelcomeView_Previews: PreviewProvider { static var previews: some View { - GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData)) + GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData) } } From d6afee11bc252c34d37cdb729c3e7afca98cbcaf Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:50:53 +0700 Subject: [PATCH 11/29] desktop: prevent clicking enter on alert and text field at the same time (#3714) --- .../kotlin/chat/simplex/common/views/helpers/AlertManager.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 082d733205..a4cea68ff2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -22,6 +22,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow class AlertManager { @@ -128,6 +129,8 @@ class AlertManager { ) { val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { + // Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard + delay(200) focusRequester.requestFocus() } TextButton(onClick = { @@ -195,6 +198,8 @@ class AlertManager { AlertContent(text, hostDevice, extraPadding = true) { val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { + // Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard + delay(200) focusRequester.requestFocus() } Row( From 3e0b863b642e6dd3476dd34dcab6560bd65e4c6d Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:57:38 +0400 Subject: [PATCH 12/29] core: add cChatJsonLength function (#3753) --- src/Simplex/Chat/Mobile.hs | 7 +++++++ tests/MobileTests.hs | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 7c74a7325a..57e062cd0d 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -20,6 +20,7 @@ import qualified Data.ByteArray as BA import qualified Data.ByteString.Base64.URL as U import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB import Data.Functor (($>)) import Data.List (find) import qualified Data.List.NonEmpty as L @@ -94,6 +95,8 @@ foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CStrin foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString +foreign export ccall "chat_json_length" cChatJsonLength :: CString -> IO CInt + foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString @@ -176,6 +179,10 @@ cChatPasswordHash cPwd cSalt = do cChatValidName :: CString -> IO CString cChatValidName cName = newCString . mkValidName =<< peekCString cName +-- | returns length of JSON encoded string +cChatJsonLength :: CString -> IO CInt +cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s + mobileChatOpts :: String -> ChatOpts mobileChatOpts dbFilePrefix = ChatOpts diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 0ed1b30f5d..2fb4446a63 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -68,6 +68,8 @@ mobileTests = do it "no exception on missing file" testMissingFileEncryptionCApi describe "validate name" $ do it "should convert invalid name to a valid name" testValidNameCApi + describe "JSON length" $ do + it "should compute length of JSON encoded string" testChatJsonLengthCApi noActiveUser :: LB.ByteString noActiveUser = @@ -356,6 +358,13 @@ testValidNameCApi _ = do cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 " peekCString cName2 `shouldReturn` goodName +testChatJsonLengthCApi :: FilePath -> IO () +testChatJsonLengthCApi _ = do + cInt1 <- cChatJsonLength =<< newCString "Hello!" + cInt1 `shouldBe` 6 + cInt2 <- cChatJsonLength =<< newCString "こんにちは!" + cInt2 `shouldBe` 18 + jDecode :: FromJSON a => String -> IO (Maybe a) jDecode = pure . J.decode . LB.pack From 430dc5bd2eebb3b97ba4df88edeec3d64abf6748 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 26 Jan 2024 00:48:40 +0700 Subject: [PATCH 13/29] ui: don't show context menu on non-sent yet live message (#3754) * android, desktop: don't show context menu on non-sent yet live message * ios: don't show context menu on non-sent yet live message --------- Co-authored-by: Avently --- apps/ios/Shared/Views/Chat/ChatView.swift | 4 +++- .../chat/simplex/common/views/chat/item/ChatItemView.kt | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 0915d62873..35caf655e9 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -749,7 +749,9 @@ struct ChatView: View { if ci.meta.editable && !mc.isVoice && !live { menu.append(editAction(ci)) } - menu.append(viewInfoUIAction(ci)) + if !ci.isLiveDummy { + menu.append(viewInfoUIAction(ci)) + } if revealed { menu.append(hideUIAction()) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 93d5430a26..354f6b1360 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -177,7 +177,8 @@ fun ChatItemView( fun MsgContentItemDropdownMenu() { val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) when { - cItem.content.msgContent != null -> { + // cItem.id check is a special case for live message chat item which has negative ID while not sent yet + cItem.content.msgContent != null && cItem.id >= 0 -> { DefaultDropdownMenu(showMenu) { if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { MsgReactionsMenu() From cd349e80ce1da84bf43db741cf94456e8d771600 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 26 Jan 2024 00:51:20 +0700 Subject: [PATCH 14/29] desktop: propertly updating delivery tab of chat item info page (#3752) --- .../chat/simplex/common/views/chat/ChatItemInfoView.kt | 2 +- .../kotlin/chat/simplex/common/views/chat/ChatView.kt | 7 +++++-- .../kotlin/chat/simplex/common/views/helpers/ModalView.kt | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index a468214528..4347623bd1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -324,7 +324,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d .fillMaxHeight(), verticalArrangement = Arrangement.SpaceBetween ) { - LaunchedEffect(Unit) { + LaunchedEffect(ciInfo) { if (ciInfo.memberDeliveryStatuses != null) { selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 6c516009fd..9969bc7f7b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -406,12 +406,15 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) } ModalManager.end.closeModals() - ModalManager.end.showModal(endButtons = { + ModalManager.end.showModalCloseable(endButtons = { ShareButton { clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get())) } - }) { + }) { close -> ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) + KeyChangeEffect(chatModel.chatId.value) { + close() + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index f41d217646..e2dd315fb0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -61,10 +61,10 @@ class ModalManager(private val placement: ModalPlacement? = null) { } } - fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) { + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { val data = ModalData() showCustomModal { close -> - ModalView(close, showClose = showClose, content = { data.content(close) }) + ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) }) } } From f102f3914708854d89be0bb3d8fac22cc7fc1e1c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 26 Jan 2024 03:59:02 +0700 Subject: [PATCH 15/29] android, desktop: protocol servers fix (#3755) Co-authored-by: Evgeny Poberezkin --- .../chat/simplex/common/model/ChatModel.kt | 2 -- .../views/usersettings/NetworkAndServers.kt | 4 --- .../views/usersettings/ProtocolServersView.kt | 26 +++++++------------ 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index b68d098f91..bec76f7c76 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -63,8 +63,6 @@ object ChatModel { val terminalItems = mutableStateOf>(listOf()) val userAddress = mutableStateOf(null) - // Allows to temporary save servers that are being edited on multiple screens - val userSMPServersUnsaved = mutableStateOf<(List)?>(null) val chatItemTTL = mutableStateOf(ChatItemTTL.None) // set when app opened from external intent diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index 7c921d7e8c..66b4a0e839 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -47,10 +47,6 @@ fun NetworkAndServersView( val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } - LaunchedEffect(Unit) { - chatModel.userSMPServersUnsaved.value = null - } - val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } NetworkAndServersLayout( currentRemoteHost = currentRemoteHost, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt index e1668ab9ad..ad1648f1ea 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt @@ -28,19 +28,18 @@ import chat.simplex.res.MR @Composable fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { var presetServers by remember(rhId) { mutableStateOf(emptyList()) } - var servers by remember(rhId) { - mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList()) - } + var servers by remember { stateGetOrPut("servers") { emptyList() } } + var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } } val currServers = remember(rhId) { mutableStateOf(servers) } val testing = rememberSaveable(rhId) { mutableStateOf(false) } - val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } } - val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } } - val saveDisabled = remember { + val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } } + val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } } + val saveDisabled = remember(servers) { derivedStateOf { servers.isEmpty() || servers == currServers.value || testing.value || - !servers.all { srv -> + servers.none { srv -> val address = parseServerAddress(srv.server) address != null && uniqueAddress(srv, address, servers) } || @@ -49,8 +48,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser } KeyChangeEffect(rhId) { - m.userSMPServersUnsaved.value = null servers = emptyList() + serversAlreadyLoaded = false } LaunchedEffect(rhId) { @@ -59,8 +58,9 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser if (res != null) { currServers.value = res.protoServers presetServers = res.presetServers - if (servers.isEmpty()) { + if (servers.isEmpty() && !serversAlreadyLoaded) { servers = currServers.value + serversAlreadyLoaded = true } } } @@ -80,13 +80,11 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser newServers.add(index, updated) old = updated servers = newServers - m.userSMPServersUnsaved.value = servers }, onDelete = { val newServers = ArrayList(servers) newServers.removeAt(index) servers = newServers - m.userSMPServersUnsaved.value = servers close() }) } @@ -125,7 +123,6 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser ScanProtocolServer(rhId) { close() servers = servers + it - m.userSMPServersUnsaved.value = servers } } } @@ -150,13 +147,11 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser testServersJob.value = withLongRunningApi { testServers(testing, servers, m) { servers = it - m.userSMPServersUnsaved.value = servers } } }, resetServers = { - servers = currServers.value ?: emptyList() - m.userSMPServersUnsaved.value = null + servers = currServers.value }, saveSMPServers = { saveServers(rhId, serverProtocol, currServers, servers, m) @@ -355,7 +350,6 @@ private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: Muta withBGApi { if (m.controller.setUserProtoServers(rhId, protocol, servers)) { currServers.value = servers - m.userSMPServersUnsaved.value = null } afterSave() } From 78a38cb0806ef4f37359ac3f6bd9e4749d3cfe7c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:37:49 +0400 Subject: [PATCH 16/29] ios: group welcome message byte limit (#3751) * ios: group welcome message character limit * confirmation dialogue key * use chatJsonLength * text * change footer --------- Co-authored-by: Evgeny Poberezkin --- .../Views/Chat/Group/GroupWelcomeView.swift | 24 ++++++++++++++----- apps/ios/SimpleXChat/API.swift | 5 ++++ apps/ios/SimpleXChat/SimpleX.h | 1 + 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index c69cc526d4..d6dbf06efc 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -18,6 +18,8 @@ struct GroupWelcomeView: View { @FocusState private var keyboardVisible: Bool @State private var showSaveDialog = false + let maxByteCount = 1200 + var body: some View { VStack { if groupInfo.canEdit { @@ -29,9 +31,12 @@ struct GroupWelcomeView: View { showSaveDialog = true } }) - .confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) { - Button("Save and update group profile") { - save() + .confirmationDialog( + welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long", + isPresented: $showSaveDialog + ) { + if welcomeTextFitsLimit() { + Button("Save and update group profile") { save() } } Button("Exit without saving") { dismiss() } } @@ -53,7 +58,7 @@ struct GroupWelcomeView: View { private func textPreview() -> some View { messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false) - .frame(minHeight: 140, alignment: .topLeading) + .frame(minHeight: 130, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .leading) } @@ -73,7 +78,7 @@ struct GroupWelcomeView: View { } .padding(.horizontal, -5) .padding(.top, -8) - .frame(height: 140, alignment: .topLeading) + .frame(height: 130, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .leading) } } else { @@ -92,6 +97,9 @@ struct GroupWelcomeView: View { } .disabled(welcomeText.isEmpty) copyButton() + } footer: { + Text(!welcomeTextFitsLimit() ? "Message too large" : "") + .foregroundColor(.red) } Section { @@ -112,13 +120,17 @@ struct GroupWelcomeView: View { Button("Save and update group profile") { save() } - .disabled(welcomeTextUnchanged()) + .disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit()) } private func welcomeTextUnchanged() -> Bool { welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) } + private func welcomeTextFitsLimit() -> Bool { + chatJsonLength(welcomeText) <= maxByteCount + } + private func save() { Task { do { diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index ab069f24cd..c0bb298929 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -105,6 +105,11 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? { return nil } +public func chatJsonLength(_ s: String) -> Int { + var c = s.cString(using: .utf8)! + return Int(chat_json_length(&c)) +} + struct ParsedMarkdown: Decodable { var formattedText: [FormattedText]? } diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index c49d104514..153365424e 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -25,6 +25,7 @@ extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_valid_name(char *name); +extern int chat_json_length(char *str); extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len); From 0f0f65533a10bb18feb777ce450f4a45991d2e12 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:57:04 +0400 Subject: [PATCH 17/29] android: group welcome message byte limit (#3756) * android: group welcome message byte limit * text * change footer --- .../src/commonMain/cpp/android/simplex-api.c | 9 ++++ .../src/commonMain/cpp/desktop/simplex-api.c | 9 ++++ .../chat/simplex/common/platform/Core.kt | 1 + .../views/chat/group/WelcomeMessageView.kt | 48 +++++++++++++++++-- .../simplex/common/views/helpers/Section.kt | 8 ++-- .../commonMain/resources/MR/base/strings.xml | 2 + 6 files changed, 69 insertions(+), 8 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index 5936bd5ff2..d0581b4336 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -66,6 +66,7 @@ extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); +extern int chat_json_length(const char *str); extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length); extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path); @@ -163,6 +164,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz return res; } +JNIEXPORT int JNICALL +Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) { + const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + int res = chat_json_length(_str); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) { const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index f15689285a..90504e25c1 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -39,6 +39,7 @@ extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); +extern int chat_json_length(const char *str); extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length); extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path); @@ -173,6 +174,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz return res; } +JNIEXPORT int JNICALL +Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) { + const char *_str = encode_to_utf8_chars(env, str); + int res = chat_json_length(_str); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) { const char *_path = encode_to_utf8_chars(env, path); diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index ec81e54418..0b5ce25044 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -28,6 +28,7 @@ external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String external fun chatPasswordHash(pwd: String, salt: String): String external fun chatValidName(name: String): String +external fun chatJsonLength(str: String): Int external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String external fun chatReadFile(path: String, key: String, nonce: String): Array external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 9124eed4c3..6c2c37503c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.chat.group import SectionBottomSpacer import SectionDividerSpaced import SectionItemView +import SectionTextFooter import SectionView import TextIconSpaced import androidx.compose.foundation.layout.* @@ -14,6 +15,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString @@ -27,9 +29,13 @@ import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.model.GroupInfo +import chat.simplex.common.platform.chatJsonLength +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.res.MR import kotlinx.coroutines.delay +private const val maxByteCount = 1200 + @Composable fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) { var gInfo by remember { mutableStateOf(groupInfo) } @@ -54,8 +60,11 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () ModalView( close = { - if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close() - else showUnsavedChangesAlert({ save(close) }, close) + when { + welcomeTextUnchanged(welcomeText, gInfo) -> close() + !welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close) + else -> showUnsavedChangesAlert({ save(close) }, close) + } }, ) { GroupWelcomeLayout( @@ -67,6 +76,14 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () } } +private fun welcomeTextUnchanged(welcomeText: MutableState, groupInfo: GroupInfo): Boolean { + return welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null) +} + +private fun welcomeTextFitsLimit(welcomeText: MutableState): Boolean { + return chatJsonLength(welcomeText.value) <= maxByteCount +} + @Composable private fun GroupWelcomeLayout( welcomeText: MutableState, @@ -95,6 +112,13 @@ private fun GroupWelcomeLayout( } else { TextPreview(wt.value, linkMode) } + SectionTextFooter( + if (!welcomeTextFitsLimit(wt)) { generalGetString(MR.strings.message_too_large) } else "", + color = if (welcomeTextFitsLimit(wt)) MaterialTheme.colors.secondary else Color.Red + ) + + Spacer(Modifier.size(8.dp)) + ChangeModeButton( editMode.value, click = { @@ -104,10 +128,18 @@ private fun GroupWelcomeLayout( ) val clipboard = LocalClipboardManager.current CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) } - SectionDividerSpaced(maxBottomPadding = false) + + Divider( + Modifier.padding( + start = DEFAULT_PADDING_HALF, + top = 8.dp, + end = DEFAULT_PADDING_HALF, + bottom = 8.dp) + ) + SaveButton( save = save, - disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null) + disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt) ) } else { val clipboard = LocalClipboardManager.current @@ -182,3 +214,11 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { onDismiss = revert, ) } + +private fun showUnsavedChangesTooLongAlert(revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.welcome_message_is_too_long), + confirmText = generalGetString(MR.strings.exit_without_saving), + onConfirm = revert, + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index ba0edb98d1..1c3540d7f2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -198,16 +198,16 @@ fun SectionItemWithValue( } @Composable -fun SectionTextFooter(text: String) { - SectionTextFooter(AnnotatedString(text)) +fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) { + SectionTextFooter(AnnotatedString(text), color = color) } @Composable -fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) { +fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) { Text( text, Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), - color = MaterialTheme.colors.secondary, + color = color, lineHeight = 18.sp, fontSize = 14.sp, textAlign = textAlign diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index d36eca273e..6fa106120c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1377,9 +1377,11 @@ Welcome message Save welcome message? + Welcome message is too long Save and update group profile Preview Enter welcome message… + Message too large SERVERS From 71924483033450de32084697078266612c9358b2 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 26 Jan 2024 17:56:17 +0400 Subject: [PATCH 18/29] core: fix invitation as rejected when deleting group (#3759) --- src/Simplex/Chat.hs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 564db9b42c..67a63dcbe0 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1035,6 +1035,7 @@ processChatCommand' vr = \case withStore' $ \db -> deleteGroup db user gInfo let contactIds = mapMaybe memberContactId members deleteAgentConnectionsAsync user . concat =<< mapM deleteUnusedContact contactIds + updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure () pure $ CRGroupDeletedUser user gInfo where deleteUnusedContact :: ContactId -> m [ConnId] @@ -1686,17 +1687,9 @@ processChatCommand' vr = \case createMemberConnection db userId fromMember agentConnId (fromJVersionRange peerChatVRange) subMode updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted - updateCIGroupInvitationStatus user + updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure () pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct - where - updateCIGroupInvitationStatus user = do - AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId - case (cInfo, content) of - (DirectChat ct, CIRcvGroupInvitation ciGroupInv memRole) -> do - let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = CIGISAccepted} memRole - updateDirectChatItemView user ct itemId aciContent False Nothing - _ -> pure () -- prohibited APIMemberRole groupId memberId memRole -> withUser $ \user -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId if memberId == groupMemberId' membership @@ -2512,6 +2505,14 @@ processChatCommand' vr = \case cReqHashes :: (ConnReqUriHash, ConnReqUriHash) cReqHashes = bimap hash hash cReqSchemas hash = ConnReqUriHash . C.sha256Hash . strEncode + updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do + AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId + case (cInfo, content) of + (DirectChat ct, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole) + | status == CIGISPending -> do + let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = newStatus} memRole + updateDirectChatItemView user ct itemId aciContent False Nothing + _ -> pure () -- prohibited toggleNtf :: ChatMonad m => User -> GroupMember -> Bool -> m () toggleNtf user m ntfOn = From 520d8868ef6426333ff8a3d8a3d0dcc2869e3fff Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:00:53 +0700 Subject: [PATCH 19/29] android: refactor clipboard access to prevent Android error in logs (#3758) --- .../src/main/java/chat/simplex/app/MainActivity.kt | 14 +++++++++++++- .../src/main/java/chat/simplex/app/SimplexApp.kt | 7 ------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index 8d64ae3c80..7a1299c612 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -1,11 +1,12 @@ package chat.simplex.app +import android.content.Context import android.content.Intent import android.net.Uri import android.os.* import android.view.WindowManager import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.ui.platform.ClipboardManager import androidx.fragment.app.FragmentActivity import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager.getUserIdFromIntent @@ -58,6 +59,17 @@ class MainActivity: FragmentActivity() { override fun onResume() { super.onResume() AppLock.recheckAuthState() + withApi { + delay(1000) + if (!isAppOnForeground) return@withApi + /** + * When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies + * access to clipboard because the app considered in background. + * This will ensure that the app will get the event on resume + * */ + val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + chatModel.clipboardHasText.value = service.hasPrimaryClip() + } } override fun onPause() { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index a99cb11488..e9f28a8ea7 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -97,13 +97,6 @@ class SimplexApp: Application(), LifecycleEventObserver { } Lifecycle.Event.ON_RESUME -> { isAppOnForeground = true - /** - * When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies - * access to clipboard because the app considered in background. - * This will ensure that the app will get the event on resume - * */ - val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - chatModel.clipboardHasText.value = service.hasPrimaryClip() if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) { SimplexService.showBackgroundServiceNoticeIfNeeded() } From 0e585d5e5b69448d1e97f59b8628c263adab1d4f Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 26 Jan 2024 20:30:21 +0400 Subject: [PATCH 20/29] core: fix group invitation marked deleted --- src/Simplex/Chat.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 67a63dcbe0..dcd392629c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1028,6 +1028,7 @@ processChatCommand' vr = \case when (memberActive membership && isOwner) . void $ sendGroupMessage' user gInfo members XGrpDel deleteGroupLinkIfExists user gInfo deleteMembersConnections user members + updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure () -- functions below are called in separate transactions to prevent crashes on android -- (possibly, race condition on integrity check?) withStore' $ \db -> deleteGroupConnectionsAndFiles db user gInfo members @@ -1035,7 +1036,6 @@ processChatCommand' vr = \case withStore' $ \db -> deleteGroup db user gInfo let contactIds = mapMaybe memberContactId members deleteAgentConnectionsAsync user . concat =<< mapM deleteUnusedContact contactIds - updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure () pure $ CRGroupDeletedUser user gInfo where deleteUnusedContact :: ContactId -> m [ConnId] From 6b8fc6fdcf8709d6b1b6a319d80d2ae7ba36629c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 27 Jan 2024 01:14:53 +0700 Subject: [PATCH 21/29] android, desktop: lower limit of terminal items for non-developers (#3763) --- .../commonMain/kotlin/chat/simplex/common/model/ChatModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index bec76f7c76..1643ddcee5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -642,7 +642,8 @@ object ChatModel { } fun addTerminalItem(item: TerminalItem) { - if (terminalItems.value.size >= 500) { + val maxItems = if (appPreferences.developerTools.get()) 500 else 200 + if (terminalItems.value.size >= maxItems) { terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size) } terminalItems.value += item From f2d498dd79ddacb76c01fe10dc6b904edf51ce97 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 27 Jan 2024 01:15:20 +0700 Subject: [PATCH 22/29] android, desktop: removed timer for some long running jobs (#3761) --- .../android/src/main/java/chat/simplex/app/SimplexService.kt | 2 +- .../commonMain/kotlin/chat/simplex/common/platform/Core.kt | 2 +- .../kotlin/chat/simplex/common/platform/NtfManager.kt | 4 ++-- .../simplex/common/views/chat/group/AddGroupMembersView.kt | 2 +- .../simplex/common/views/database/DatabaseEncryptionView.kt | 2 +- .../kotlin/chat/simplex/common/views/database/DatabaseView.kt | 4 ++-- .../chat/simplex/common/views/localauth/LocalAuthView.kt | 2 +- .../common/views/onboarding/SetupDatabasePassphrase.kt | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index c58959d5cc..903f096080 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -104,7 +104,7 @@ class SimplexService: Service() { if (wakeLock != null || isStartingService) return val self = this isStartingService = true - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi { val chatController = ChatController waitDbMigrationEnds(chatController) try { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 0b5ce25044..96af131a21 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -43,7 +43,7 @@ val appPreferences: AppPreferences val chatController: ChatController = ChatController fun initChatControllerAndRunMigrations() { - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { initChatController(startChat = ::showStartChatAfterRestartAlert) } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index a75ee75903..57c1e578ae 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -55,7 +55,7 @@ abstract class NtfManager { } fun openChatAction(userId: Long?, chatId: ChatId) { - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi { awaitChatStartedIfNeeded(chatModel) if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { // TODO include remote host ID in desktop notifications? @@ -70,7 +70,7 @@ abstract class NtfManager { } fun showChatsAction(userId: Long?) { - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi { awaitChatStartedIfNeeded(chatModel) if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { // TODO include remote host ID in desktop notifications? diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index dcf3b36a5b..6add33d83d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -54,7 +54,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea }, inviteMembers = { allowModifyMembers = false - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi(slow = 30_000, deadlock = 120_000) { for (contactId in selectedContacts) { val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) if (member != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index 3cfd9e94b4..7bd9fbc66f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -62,7 +62,7 @@ fun DatabaseEncryptionView(m: ChatModel) { initialRandomDBPassphrase, progressIndicator, onConfirmEncrypt = { - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi { encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 2d644b297e..8680c98d46 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -368,7 +368,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { } fun startChat(m: ChatModel, chatLastStart: MutableState, chatDbChanged: MutableState, progressIndicator: MutableState? = null) { - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi { try { progressIndicator?.value = true if (chatDbChanged.value) { @@ -581,7 +581,7 @@ private fun importArchive( progressIndicator.value = true val archivePath = saveArchiveFromURI(importedArchiveURI) if (archivePath != null) { - withLongRunningApi(slow = 60_000, deadlock = 180_000) { + withLongRunningApi { try { m.controller.apiDeleteStorage() try { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt index 1048b03bc0..5a37c860a0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt @@ -49,7 +49,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { } private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) { - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi { try { /** Waiting until [initChatController] finishes */ while (m.ctrlInitInProgress.value) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index 8547704893..9ae34eb180 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -50,7 +50,7 @@ fun SetupDatabasePassphrase(m: ChatModel) { confirmNewKey, progressIndicator, onConfirmEncrypt = { - withLongRunningApi(slow = 30_000, deadlock = 60_000) { + withLongRunningApi { if (m.chatRunning.value == true) { // Stop chat if it's started before doing anything stopChatAsync(m) From a1328c287c9c2bd4d975da4e81a7e0b4bdecfedc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 26 Jan 2024 18:54:08 +0000 Subject: [PATCH 23/29] core: remove unused events from api (#3764) * core: remove unused events from api * fix test --- src/Simplex/Chat/Mobile.hs | 11 ++++++++++- tests/MobileTests.hs | 2 -- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 57e062cd0d..105dedb32c 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -271,9 +271,18 @@ chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO chatSendRemoteCmd cc rh s = J.encode . APIResponse Nothing rh <$> runReaderT (execChatCommand rh s) cc chatRecvMsg :: ChatController -> IO JSONByteString -chatRecvMsg ChatController {outputQ} = json <$> atomically (readTBQueue outputQ) +chatRecvMsg ChatController {outputQ} = json <$> readChatResponse where json (corr, remoteHostId, resp) = J.encode APIResponse {corr, remoteHostId, resp} + readChatResponse = do + out@(_, _, cr) <- atomically $ readTBQueue outputQ + if filterEvent cr then pure out else readChatResponse + filterEvent = \case + CRGroupSubscribed {} -> False + CRGroupEmpty {} -> False + CRMemberSubSummary {} -> False + CRPendingSubSummary {} -> False + _ -> True chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc) diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 2fb4446a63..f41d0172de 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -224,8 +224,6 @@ testChatApi tmp = do chatSendCmd cc "/_start" `shouldReturn` chatStarted chatRecvMsg cc `shouldReturn` networkStatuses chatRecvMsg cc `shouldReturn` userContactSubSummary - chatRecvMsg cc `shouldReturn` memberSubSummary - chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary chatRecvMsgWait cc 10000 `shouldReturn` "" chatParseMarkdown "hello" `shouldBe` "{}" chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown From 2b3eebb7a299367a244d6722be53526b23f9f167 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 27 Jan 2024 06:08:48 +0700 Subject: [PATCH 24/29] android, desktop: show different text when database migrates (#3762) * android, desktop: show different text when database migrates * one more check * refactor --------- Co-authored-by: Evgeny Poberezkin --- .../kotlin/chat/simplex/common/App.kt | 1 + .../chat/simplex/common/model/ChatModel.kt | 1 + .../chat/simplex/common/platform/Core.kt | 18 ++++++++++++++++-- .../common/views/helpers/DefaultProgressBar.kt | 3 ++- .../commonMain/resources/MR/base/strings.xml | 1 + 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index bfff3bf9fb..57959af4c6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -108,6 +108,7 @@ fun MainScreen() { val localUserCreated = chatModel.localUserCreated.value var showInitializationView by remember { mutableStateOf(false) } when { + chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress)) chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database)) showChatDatabaseError -> { // Prevent showing keyboard on Android when: passcode enabled and database password not saved diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 1643ddcee5..54d4a95ffb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -48,6 +48,7 @@ object ChatModel { val chatDbEncrypted = mutableStateOf(false) val chatDbStatus = mutableStateOf(null) val ctrlInitInProgress = mutableStateOf(false) + val dbMigrationInProgress = mutableStateOf(false) val chats = mutableStateListOf() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 96af131a21..63fcb90bbe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -59,10 +59,23 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat chatModel.ctrlInitInProgress.value = true val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp - val migrated: Array = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) - val res: DBMigrationResult = kotlin.runCatching { + var migrated: Array = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value) + var res: DBMigrationResult = runCatching { json.decodeFromString(migrated[0] as String) }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } + val rerunMigration = res is DBMigrationResult.ErrorMigration && when (res.migrationError) { + // we don't allow to run down migrations without confirmation in UI, so currently it won't be YesUpDown + is MigrationError.Upgrade -> confirm == MigrationConfirmation.YesUp || confirm == MigrationConfirmation.YesUpDown + is MigrationError.Downgrade -> confirm == MigrationConfirmation.YesUpDown + is MigrationError.Error -> false + } + if (rerunMigration) { + chatModel.dbMigrationInProgress.value = true + migrated = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) + res = runCatching { + json.decodeFromString(migrated[0] as String) + }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } + } val ctrl = if (res is DBMigrationResult.OK) { migrated[1] as Long } else null @@ -120,6 +133,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } } finally { chatModel.ctrlInitInProgress.value = false + chatModel.dbMigrationInProgress.value = false } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultProgressBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultProgressBar.kt index ec2500ab2e..104a01150f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultProgressBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultProgressBar.kt @@ -5,6 +5,7 @@ import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import chat.simplex.common.ui.theme.DEFAULT_PADDING @@ -20,7 +21,7 @@ fun DefaultProgressView(description: String?) { strokeWidth = 2.5.dp ) if (description != null) { - Text(description) + Text(description, textAlign = TextAlign.Center) } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 6fa106120c..a511e2e13d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -16,6 +16,7 @@ Opening database… + Database migration is in progress.\nIt may take a few minutes. Invalid file path You shared an invalid file path. Report the issue to the app developers. View crashed From c4cbb49f5756e6ba00f028a13af1d0048aac47a7 Mon Sep 17 00:00:00 2001 From: "M. Sarmad Qadeer" Date: Sat, 27 Jan 2024 21:51:21 +0500 Subject: [PATCH 25/29] website: update contact page layout if JS is disabled (#3331) * website: update page layout for the case if javascript is disabled in browser. * website: add noscript tag & update heading * website: do some changes in layout of contact page without JS --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin --- website/langs/en.json | 4 +++- website/src/_includes/contact_page.html | 20 ++++++++++++++++---- website/src/css/style.css | 4 ++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/website/langs/en.json b/website/langs/en.json index 1bb64c7efa..10db2dd4ed 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -250,5 +250,7 @@ "stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org", "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please export the chat database and re-install the app.", - "jobs": "Join team" + "jobs": "Join team", + "please-enable-javascript": "Please enable JavaScript to see the QR code.", + "please-use-link-in-mobile-app": "Please use the link in the mobile app" } diff --git a/website/src/_includes/contact_page.html b/website/src/_includes/contact_page.html index 6beb148f8d..b5f7442a75 100644 --- a/website/src/_includes/contact_page.html +++ b/website/src/_includes/contact_page.html @@ -30,8 +30,12 @@
+ + -
+

{{ "scan-qr-code-from-mobile-app" | i18n({}, lang ) | safe }}

@@ -61,7 +65,11 @@
-

{{ "connect-in-app" | i18n({}, lang ) | safe }}

+

{{ "connect-in-app" | i18n({}, lang ) | safe }}

+ + {{ "open-simplex-app" | i18n({}, lang ) | safe }}
@@ -69,7 +77,7 @@
-
+

{{ "tap-the-connect-button-in-the-app" | i18n({}, lang ) | safe }}

@@ -81,7 +89,7 @@ -