From 6c2679fa1998978e6dc5a06003c5dede3e996a0f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 23 Sep 2026 08:45:37 +0100 Subject: [PATCH 1/9] website: cf page headline (#7564) Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- website/src/crowdfunding.html | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/website/src/crowdfunding.html b/website/src/crowdfunding.html index 177f722097..e7622a2ad6 100644 --- a/website/src/crowdfunding.html +++ b/website/src/crowdfunding.html @@ -170,7 +170,7 @@ templateEngineOverride: njk .cf-hero-text { margin-left: calc(var(--sec-vwu) * 7.5); - width: calc(var(--sec-vwu) * 45); + width: calc(var(--sec-vwu) * 53); position: relative; z-index: 4; } @@ -178,7 +178,7 @@ templateEngineOverride: njk .cf-hero h1 { font-family: "GT-Walsheim", sans-serif; font-weight: 400; - font-size: calc(var(--sec-vwu) * 4.94); + font-size: calc(var(--sec-vwu) * 4.45); line-height: 1.05; letter-spacing: -0.025em; color: #ffffff; @@ -194,6 +194,7 @@ templateEngineOverride: njk max-width: calc(var(--sec-vwu) * 40); } .cf-hero .lead-short { display: none; } + .cf-hero h1 .cf-br-narrow { display: none; } .cf-stats { display: flex; @@ -654,7 +655,7 @@ templateEngineOverride: njk --cf-phone-h: calc(var(--cf-phone-w) / 0.4914); --cf-phone-part: 0.55; /* bottom of the hero text block, measured from the section top */ - --cf-hero-text-b: calc(var(--cf-top-base) + max(0px, calc(-1 * var(--cf-area-off))) + 76.7vw); + --cf-hero-text-b: calc(var(--cf-top-base) + max(0px, calc(-1 * var(--cf-area-off))) + 92.5vw); /* the arc crest starts 0.2678 down the cover image, which is 2.349 phone widths tall and sits 0.4034 phone heights above the phone top: the phone may rise no higher than this */ @@ -681,7 +682,8 @@ templateEngineOverride: njk .cf-hero h1, .cf-hero .lead, .cf-hero .cf-stats { position: relative; z-index: 4; } - .cf-hero h1 { font-size: calc(var(--sec-vwu) * 8.54); } + .cf-hero h1 { font-size: calc(var(--sec-vwu) * 9); } + .cf-hero h1 .cf-br-narrow { display: inline; } .cf-hero .lead { font-size: calc(var(--sec-vwu) * 3.55); max-width: none; margin-top: calc(var(--sec-vhu) * 2); } .cf-hero .lead:not(.lead-short) { display: none; } .cf-hero .lead-short { display: block; font-size: calc(var(--sec-vwu) * 4.5); white-space: nowrap; } @@ -824,9 +826,16 @@ templateEngineOverride: njk .cf-tag .n { font-size: 3.54cqh; line-height: 1.3; } } + /* tall phones (modern ratios, with or without the browser bars): the headline takes the + room the screen has; short ones keep the smaller size. */ + @media screen and (max-width: 959px) and (orientation: portrait) and (max-aspect-ratio: 62/100) { + .cf-hero h1 { font-size: calc(var(--sec-vwu) * 9.6); } + :root { --cf-hero-text-b: calc(var(--cf-top-base) + max(0px, calc(-1 * var(--cf-area-off))) + 91vw); } + } + /* 320pt phones: keep the headline at three lines */ @media screen and (max-width: 330px) and (orientation: portrait) { - .cf-hero h1 { font-size: calc(var(--sec-vwu) * 8.18); } + .cf-hero h1 { font-size: calc(var(--sec-vwu) * 8.6); } } /* narrow phones: keep the corner link clear of the logo */ @@ -1041,7 +1050,7 @@ templateEngineOverride: njk .page .text-container p { font-weight: 200; font-size: calc(var(--sec-vwu) * 1.62); } .page .text-container p span { font-weight: 500; } .page .text-container a { font-weight: 200; font-size: calc(var(--sec-vwu) * 1.62); } - .cf-hero h1 { font-size: calc(var(--sec-vwu) * 4.4); } + .cf-hero h1 { font-size: calc(var(--sec-vwu) * 4.3); } .cf-hero .lead:not(.lead-short) { display: none; } .cf-hero .lead-short { display: block; margin-top: calc(var(--sec-vhu) * 2); } .cf-hero .cf-hero-cta { margin-top: calc(var(--sec-vhu) * 3); } @@ -1078,8 +1087,8 @@ templateEngineOverride: njk
-

The first and the only messaging network without any user IDs

-

SimpleX Chat is building a messaging network unlike every major platform — without phone numbers, usernames, emails, or any user identifiers.

+

The only
messaging network
without any
user identifiers

+

SimpleX Chat is building a messaging network unlike every major platform — without phone numbers, usernames, emails, or user accounts.

No phone numbers, emails, or accounts.

From 0b9ae4d6def34c092a06e662560acf81af44944d Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:34:47 +0000 Subject: [PATCH 2/9] ui: open support SimpleX screen from badge and file alerts (#7563) --- apps/ios/Shared/Views/Badges/BadgesView.swift | 20 +++++++++++++ .../Shared/Views/Chat/ChatItemInfoView.swift | 9 ++++++ .../Chat/ComposeMessage/ComposeView.swift | 17 ++++++++--- .../Shared/Views/ChatList/ChatListView.swift | 10 ++----- apps/ios/Shared/Views/Helpers/AppSheet.swift | 12 ++++++++ apps/ios/Shared/Views/Helpers/NameBadge.swift | 9 ++++-- apps/ios/spec/client/chat-view.md | 1 + .../views/badges/BadgesRedeemCodeView.kt | 4 +-- .../views/badges/BadgesSupportSimplexView.kt | 22 +++++++------- .../simplex/common/views/badges/BadgesView.kt | 12 ++++++-- .../views/badges/BadgesYourBadgeView.kt | 6 ++-- .../views/badges/BadgesYourLevelView.kt | 10 +++---- .../common/views/chat/ChatItemInfoView.kt | 7 +++++ .../simplex/common/views/chat/ComposeView.kt | 30 ++++++++++++++----- .../common/views/chatlist/ChatListView.kt | 6 ++-- .../common/views/helpers/ChatInfoImage.kt | 17 ++++++++--- .../common/views/usersettings/SettingsView.kt | 2 +- .../commonMain/resources/MR/ar/strings.xml | 1 - .../commonMain/resources/MR/base/strings.xml | 2 +- .../commonMain/resources/MR/de/strings.xml | 1 - .../commonMain/resources/MR/es/strings.xml | 1 - .../commonMain/resources/MR/fr/strings.xml | 1 - .../commonMain/resources/MR/hu/strings.xml | 1 - .../commonMain/resources/MR/it/strings.xml | 1 - .../commonMain/resources/MR/ja/strings.xml | 1 - .../resources/MR/pt-rBR/strings.xml | 1 - .../commonMain/resources/MR/ru/strings.xml | 1 - .../commonMain/resources/MR/tr/strings.xml | 1 - .../resources/MR/zh-rCN/strings.xml | 1 - .../resources/MR/zh-rTW/strings.xml | 1 - apps/multiplatform/spec/client/chat-view.md | 2 +- 31 files changed, 143 insertions(+), 67 deletions(-) diff --git a/apps/ios/Shared/Views/Badges/BadgesView.swift b/apps/ios/Shared/Views/Badges/BadgesView.swift index 1960d25bf0..df12094a6b 100644 --- a/apps/ios/Shared/Views/Badges/BadgesView.swift +++ b/apps/ios/Shared/Views/Badges/BadgesView.swift @@ -33,3 +33,23 @@ struct BadgesView: View { .animation(.default, value: shownBadge != nil) } } + +var supportSimpleXAlertAction: UIAlertAction { + UIAlertAction(title: NSLocalizedString("Support SimpleX", comment: "alert button"), style: .default) { _ in + openBadgesView() + } +} + +func openBadgesView() { + showAppSheet { + NavigationView { + BadgesView(showsAsSheet: true) + .modifier(ThemedBackground()) + } + } +} + +// false until the badge state is loaded for the current profile, so a supporter is never pitched to +func noShownBadge() -> Bool { + BadgeModel.shared.badgeState?.shown != true && BadgeModel.shared.userId == ChatModel.shared.currentUser?.userId +} diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 15effaeea6..ffcf525300 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -164,6 +164,15 @@ struct ChatItemInfoView: View { } if let file = ci.file, let fileExpires = file.fileExpires { infoRow(file.expired ? "File was available until" : "File available until", localTimestamp(fileExpires)) + if noShownBadge() { + Button { + openBadgesView() + } label: { + Text("Support SimpleX to send larger files that stay available longer") + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + } + } } if meta.msgVerified?.verified == true { let signedText: LocalizedStringKey = ci.chatDir.sent ? "Signed" : "Signed & verified" diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index caa4e2a501..a1570ba4a1 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -338,6 +338,18 @@ enum UploadContent: Equatable { } } +// A badge only helps below the largest badge's limit, and not in incognito chats, so outside that +// the alert is informational as before. +func showLargeFileAlert(_ fileSize: Int64, incognito: Bool, senderProfile: LocalProfile?) { + let title = NSLocalizedString("Large file!", comment: "file alert title") + let message = largeFileMessage(fileSize, incognito: incognito, badgeIssue: expiredBadgeReason(fileSize, senderProfile)) + if !incognito && fileSize <= MAX_FILE_SIZE_XFTP_LEGEND && noShownBadge() { + showAlert(title, message: message) { [supportSimpleXAlertAction, okAlertAction] } + } else { + showAlert(title, message: message) + } +} + // Spec: spec/client/compose.md#ComposeView struct ComposeView: View { @EnvironmentObject var chatModel: ChatModel @@ -668,10 +680,7 @@ struct ComposeView: View { fileSize <= maxFileSize { composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent, file: fileURL)) } else { - showAlert( - NSLocalizedString("Large file!", comment: "file alert title"), - message: largeFileMessage(Int64(fileSize ?? 0), incognito: sendIncognito, badgeIssue: expiredBadgeReason(Int64(fileSize ?? 0), sendProfile)) - ) + showLargeFileAlert(Int64(fileSize ?? 0), incognito: sendIncognito, senderProfile: sendProfile) } } catch { logger.error("ComposeView fileImporter error \(error.localizedDescription)") diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index b7017bad2b..5c5021b79f 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -393,12 +393,6 @@ struct ChatListView: View { badgeModel.alert?.kind == .issueFailed && badgeModel.userId == chatModel.currentUser?.userId } - // false until the badge state loads: if the pitch rendered before that, it would lock the slot, and a supporter's badge - // arriving a moment later would hide it, leaving the slot empty for the session - private var noShownBadge: Bool { - badgeModel.badgeState?.shown != true && badgeModel.userId == chatModel.currentUser?.userId - } - private func showBadgeAlertDismissAlert(_ title: String) { showAlert(title) { [ @@ -515,7 +509,9 @@ struct ChatListView: View { .listRowBackground(Color.clear) .zIndex(1) .onAppear { chatModel.chatListBanner = .badgeIssueFailed } - } else if chatModel.bannerSlotFree(for: .badgePitch) && !supporterBannerShown && noShownBadge && chatModel.chats.count > 3 { + // noShownBadge is false until the badge state loads: the pitch must not lock the slot and then + // be hidden by a supporter's badge arriving a moment later, leaving the slot empty for the session + } else if chatModel.bannerSlotFree(for: .badgePitch) && !supporterBannerShown && noShownBadge() && chatModel.chats.count > 3 { SupportSimpleXBanner( showDismiss: supporterBannerTapped, onTap: { diff --git a/apps/ios/Shared/Views/Helpers/AppSheet.swift b/apps/ios/Shared/Views/Helpers/AppSheet.swift index 17fe95a058..51ae63f07e 100644 --- a/apps/ios/Shared/Views/Helpers/AppSheet.swift +++ b/apps/ios/Shared/Views/Helpers/AppSheet.swift @@ -29,6 +29,18 @@ private struct PrivacySensitive: ViewModifier { } } +// Presented from the top view controller instead of a .sheet on a parent view, so an alert button or +// a view that is itself in a sheet can open it. +func showAppSheet(@ViewBuilder content: () -> Content) { + if let topController = getTopViewController() { + let v = content() + .modifier(PrivacySensitive()) + .environmentObject(ChatModel.shared) + .environmentObject(AppTheme.shared) + topController.present(UIHostingController(rootView: v), animated: true) + } +} + extension View { func appSheet( isPresented: Binding, diff --git a/apps/ios/Shared/Views/Helpers/NameBadge.swift b/apps/ios/Shared/Views/Helpers/NameBadge.swift index 26d9821158..085d9be0be 100644 --- a/apps/ios/Shared/Views/Helpers/NameBadge.swift +++ b/apps/ios/Shared/Views/Helpers/NameBadge.swift @@ -166,8 +166,13 @@ func showBadgeInfoAlert(_ name: String, _ badge: LocalBadge) { } else { String.localizedStringWithFormat(NSLocalizedString("%@ supports SimpleX Chat.", comment: "badge alert"), name) } - let v7 = NSLocalizedString("You can support SimpleX starting from v7 of the app.", comment: "badge alert") - showAlert(title, message: supports + "\n\n" + v7) + if noShownBadge() { + showAlert(title, message: supports) { + [ supportSimpleXAlertAction, okAlertAction ] + } + } else { + showAlert(title, message: supports) + } } } } diff --git a/apps/ios/spec/client/chat-view.md b/apps/ios/spec/client/chat-view.md index afe656ed04..c509662c9e 100644 --- a/apps/ios/spec/client/chat-view.md +++ b/apps/ios/spec/client/chat-view.md @@ -279,6 +279,7 @@ Detailed message information sheet (accessed via long-press menu "Info"): - Edit history (all previous versions of edited messages) - Forward chain info - Message timestamps (created, updated, deleted) +- When the file expires, and below it, for a user without a badge, a button opening the badges sheet --- diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt index a9d2a4d19c..9ebaf9bcdc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt @@ -58,7 +58,7 @@ private fun formatBadgeCodeInput(s: String): String { } @Composable -fun BadgesRedeemCodeView() { +fun BadgesRedeemCodeView(modalManager: ModalManager) { val rhId = remember { chatModel.remoteHostId() } val supporterBannerShown = remember { appPrefs.supporterBannerShown } val code = remember { mutableStateOf(TextFieldValue("")) } @@ -96,7 +96,7 @@ fun BadgesRedeemCodeView() { ) } else { supporterBannerShown.set(true) - ModalManager.start.closeModal() + modalManager.closeModal() } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt index 48ef36ece1..4f1cc2ed66 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt @@ -32,7 +32,7 @@ import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton import chat.simplex.res.MR @Composable -fun BadgesSupportSimplexView() { +fun BadgesSupportSimplexView(modalManager: ModalManager) { ColumnWithScrollBar( Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp), verticalArrangement = Arrangement.spacedBy(16.dp), @@ -57,7 +57,7 @@ fun BadgesSupportSimplexView() { // TODO [badges] restore WhyBuiltButton() when in-app purchase lands: the level screen // returns to the flow and HowItWorksButton() moves there, leaving this one alone here. - HowItWorksButton() + HowItWorksButton(modalManager) Spacer(Modifier.weight(1f)) @@ -66,7 +66,7 @@ fun BadgesSupportSimplexView() { Spacer(Modifier.weight(1f)) Column(horizontalAlignment = Alignment.CenterHorizontally) { - RedeemCodeButton() + RedeemCodeButton(modalManager) GetCodeButton() } } @@ -74,22 +74,22 @@ fun BadgesSupportSimplexView() { // the in-app purchase path, kept compiling and uncalled until payments return after the MVP @Composable -private fun ChooseLevelButton() { +private fun ChooseLevelButton(modalManager: ModalManager) { OnboardingActionButton( modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.badges_choose_your_level, onboarding = null, onclick = { - ModalManager.start.showModal { BadgesYourLevelView() } + modalManager.showModal { BadgesYourLevelView(modalManager) } } ) } @Composable -private fun WhyBuiltButton() { +private fun WhyBuiltButton(modalManager: ModalManager) { val primary = MaterialTheme.colors.primary TextButton({ - ModalManager.start.showModal { HowItWorks(user = chatModel.currentUser.value, onboardingStage = null, titleColor = primary) } + modalManager.showModal { HowItWorks(user = chatModel.currentUser.value, onboardingStage = null, titleColor = primary) } }) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary) @@ -99,9 +99,9 @@ private fun WhyBuiltButton() { } @Composable -private fun HowItWorksButton() { +private fun HowItWorksButton(modalManager: ModalManager) { TextButton({ - ModalManager.start.showModal { BadgesHowItWorksView() } + modalManager.showModal { BadgesHowItWorksView() } }) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary) @@ -111,13 +111,13 @@ private fun HowItWorksButton() { } @Composable -private fun RedeemCodeButton() { +private fun RedeemCodeButton(modalManager: ModalManager) { OnboardingActionButton( modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.badges_redeem_code_button, onboarding = null, onclick = { - ModalManager.start.showModal { BadgesRedeemCodeView() } + modalManager.showModal { BadgesRedeemCodeView(modalManager) } } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesView.kt index 0cb128c902..ff2660497e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesView.kt @@ -5,11 +5,12 @@ import androidx.compose.runtime.Composable import chat.simplex.common.model.BadgeModel import chat.simplex.common.model.BadgeState import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.helpers.ModalManager import chat.simplex.common.views.helpers.ModalView @OptIn(ExperimentalAnimationApi::class) @Composable -fun BadgesView(close: () -> Unit) { +fun BadgesView(modalManager: ModalManager, close: () -> Unit) { val shownBadge: BadgeState? = run { if (!BadgeModel.isCurrent(chatModel.remoteHostId(), chatModel.currentUser.value?.userId)) return@run null val badgeState = BadgeModel.badgeState.value @@ -20,10 +21,15 @@ fun BadgesView(close: () -> Unit) { ModalView(close, cardScreen = shownBadge != null) { AnimatedContent(targetState = shownBadge, transitionSpec = { fadeIn() with fadeOut() }, contentKey = { it != null }) { badgeState -> if (badgeState != null) { - BadgesYourBadgeView(badgeState) + BadgesYourBadgeView(badgeState, modalManager) } else { - BadgesSupportSimplexView() + BadgesSupportSimplexView(modalManager) } } } } + +// ModalManager.end, not start: every caller is in the chat, which on desktop is the right pane +fun openBadgesView() { + ModalManager.end.showCustomModal { close -> BadgesView(ModalManager.end, close) } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourBadgeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourBadgeView.kt index 34a59343ce..c2ca7d7f10 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourBadgeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourBadgeView.kt @@ -37,7 +37,7 @@ import chat.simplex.common.views.usersettings.simplexTeamUri import chat.simplex.res.MR @Composable -fun BadgesYourBadgeView(badgeState: BadgeState) { +fun BadgesYourBadgeView(badgeState: BadgeState, modalManager: ModalManager) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.badges_your_badge)) @@ -54,7 +54,7 @@ fun BadgesYourBadgeView(badgeState: BadgeState) { SettingsActionItem( painterResource(MR.images.ic_info), stringResource(MR.strings.badges_how_it_works_button), - { ModalManager.start.showModal { BadgesHowItWorksView() } }, + { modalManager.showModal { BadgesHowItWorksView() } }, ) } SectionSpacer() @@ -98,7 +98,7 @@ fun BadgesYourBadgeView(badgeState: BadgeState) { SectionItemView({ clipboard.setText(AnnotatedString(badgeState.purchaseKey)) }) { Text(stringResource(MR.strings.badges_copy_purchase_key), color = MaterialTheme.colors.primary) } - SectionItemView({ ModalManager.start.showCustomModal { close -> BadgesLedgerView(badgeState, close) } }) { + SectionItemView({ modalManager.showCustomModal { close -> BadgesLedgerView(badgeState, close) } }) { Text(stringResource(MR.strings.badges_ledger)) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourLevelView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourLevelView.kt index c77e4dd61e..161702704b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourLevelView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourLevelView.kt @@ -54,7 +54,7 @@ enum class BadgeLevel { } @Composable -fun BadgesYourLevelView() { +fun BadgesYourLevelView(modalManager: ModalManager) { var selectedLevel by remember { mutableStateOf(BadgeLevel.Supporter) } LaunchedEffect(Unit) { BadgeStore.load() } @@ -99,11 +99,11 @@ fun BadgesYourLevelView() { // Nested Column with no spacing so the TextButtonBelowOnboardingButton sits directly under // the action button (matches onboarding pattern where its own 7.5dp top padding is the gap). Column(horizontalAlignment = Alignment.CenterHorizontally) { - ContinueButton(selectedLevel) + ContinueButton(selectedLevel, modalManager) TextButtonBelowOnboardingButton( text = stringResource(MR.strings.badges_how_it_works_button), icon = painterResource(MR.images.ic_info), - onClick = { ModalManager.start.showModal { BadgesHowItWorksView() } } + onClick = { modalManager.showModal { BadgesHowItWorksView() } } ) } } @@ -140,13 +140,13 @@ private fun LevelCard(level: BadgeLevel, selectedLevel: BadgeLevel, modifier: Mo } @Composable -private fun ContinueButton(selectedLevel: BadgeLevel) { +private fun ContinueButton(selectedLevel: BadgeLevel, modalManager: ModalManager) { OnboardingActionButton( modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.badges_continue, onboarding = null, onclick = { - ModalManager.start.showModal { BadgesPayView(selectedLevel) } + modalManager.showModal { BadgesPayView(selectedLevel) } } ) } 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 554d79ebae..50f5e35462 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 @@ -29,7 +29,9 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.group.MemberProfileImage import chat.simplex.common.views.chat.item.* +import chat.simplex.common.views.badges.openBadgesView import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.newchat.noShownBadge import chat.simplex.common.views.usersettings.networkAndServers.serverHostname import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -277,6 +279,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools if (file?.fileExpires != null) { val expiresRes = if (file.expired) MR.strings.info_row_file_expired else MR.strings.info_row_file_expires InfoRow(stringResource(expiresRes), localTimestamp(file.fileExpires)) + if (noShownBadge()) { + SectionItemView(::openBadgesView) { + Text(stringResource(MR.strings.badges_larger_files_longer), color = MaterialTheme.colors.primary) + } + } } if (ci.meta.msgVerified?.verified == true) { val signedRes = if (sent) MR.strings.info_row_signed else MR.strings.info_row_signed_verified 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 a03cb63525..c81149fcb1 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 @@ -33,12 +33,14 @@ import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.filesToDelete import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.badges.openBadgesView import chat.simplex.common.views.chat.group.hostFromRelayLink import chat.simplex.common.views.chat.group.relayConnStatus import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.RelayProgressIndicator import chat.simplex.common.views.newchat.RelayStatusIndicator +import chat.simplex.common.views.newchat.noShownBadge import chat.simplex.common.views.newchat.relayDisplayName import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -320,6 +322,24 @@ private fun isVideoUri(uri: URI): Boolean { private fun isWebmUri(uri: URI): Boolean = getFileName(uri)?.lowercase()?.endsWith(".webm") == true +// A badge only helps below the largest badge's limit, and not in incognito chats, so outside that +// the alert is informational as before. +fun showLargeFileAlert(fileSize: Long, incognito: Boolean, senderProfile: LocalProfile?) { + val title = generalGetString(MR.strings.large_file) + val text = largeFileMessage(fileSize, incognito, expiredBadgeReason(fileSize, senderProfile)) + if (!incognito && fileSize <= MAX_FILE_SIZE_XFTP_LEGEND && noShownBadge()) { + AlertManager.shared.showAlertDialog( + title = title, + text = text, + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.badges_support_simplex_title), + onDismiss = ::openBadgesView + ) + } else { + AlertManager.shared.showAlertMsg(title, text) + } +} + fun MutableState.processPickedFile(uri: URI?, text: String?) { if (uri != null) { val maxFileSize = value.maxFileSize @@ -330,10 +350,7 @@ fun MutableState.processPickedFile(uri: URI?, text: String?) { value = value.copy(message = if (text != null) ComposeMessage(text) else value.message, preview = ComposePreview.FilePreview(fileName, uri)) } } else if (fileSize != null) { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.large_file), - largeFileMessage(fileSize, value.sendIncognito, expiredBadgeReason(fileSize, chatModel.currentUser.value?.profile)) - ) + showLargeFileAlert(fileSize, value.sendIncognito, chatModel.currentUser.value?.profile) } else { showWrongUriAlert() } @@ -359,10 +376,7 @@ suspend fun MutableState.processPickedMedia(uris: List, text: UploadContent.AnimatedImage(uri) } else { bitmap = null - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.large_file), - largeFileMessage(fileSize ?: 0, value.sendIncognito, expiredBadgeReason(fileSize ?: 0, chatModel.currentUser.value?.profile)) - ) + showLargeFileAlert(fileSize ?: 0, value.sendIncognito, chatModel.currentUser.value?.profile) null } } else if (bitmap != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index d0748e4a16..fae1b7c155 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -1052,7 +1052,7 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat SupportSimpleXBanner( title = stringResource(MR.strings.badges_support_ended), subtitle = String.format(stringResource(MR.strings.badges_support_ended_on), alert.dateText), - onTap = { ModalManager.start.showCustomModal { close -> BadgesView(close) } }, + onTap = { ModalManager.start.showCustomModal { close -> BadgesView(ModalManager.start, close) } }, onDismiss = { showBadgeAlertDismissAlert(generalGetString(MR.strings.badges_support_ended)) } ) } @@ -1065,7 +1065,7 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat title = stringResource(MR.strings.badges_renewal_failed), subtitle = stringResource(MR.strings.badges_tap_for_details), warning = true, - onTap = { ModalManager.start.showCustomModal { close -> BadgesView(close) } }, + onTap = { ModalManager.start.showCustomModal { close -> BadgesView(ModalManager.start, close) } }, onDismiss = { showBadgeAlertDismissAlert(generalGetString(MR.strings.badges_renewal_failed)) } ) } @@ -1078,7 +1078,7 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat showDismiss = supporterBannerTapped.value, onTap = { appPrefs.supporterBannerTapped.set(true) - ModalManager.start.showCustomModal { close -> BadgesView(close) } + ModalManager.start.showCustomModal { close -> BadgesView(ModalManager.start, close) } }, onDismiss = ::showSupportSimpleXDismissAlert ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index fcae2e6b47..91919ff0cf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -36,6 +36,8 @@ import chat.simplex.common.model.LocalBadge import chat.simplex.common.model.localDate import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.badges.openBadgesView +import chat.simplex.common.views.newchat.noShownBadge import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import kotlin.math.max @@ -244,10 +246,17 @@ fun showBadgeInfoAlert(name: String, badge: LocalBadge, uriHandler: UriHandler) String.format(generalGetString(MR.strings.badge_supported_simplex), name, localDate(badge.badge.badgeExpiry)) else String.format(generalGetString(MR.strings.badge_supports_simplex), name) - AlertManager.shared.showAlertMsg( - title = title, - text = supports + "\n\n" + generalGetString(MR.strings.badge_support_from_v7) - ) + if (noShownBadge()) { + AlertManager.shared.showAlertDialog( + title = title, + text = supports, + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.badges_support_simplex_title), + onDismiss = ::openBadgesView + ) + } else { + AlertManager.shared.showAlertMsg(title = title, text = supports) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 9e1a6eb4ad..c49caf7cfa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -107,7 +107,7 @@ fun SettingsLayout( SectionView { // Direct showModal (no settings / cardScreen flags) — settings-style card chrome would render // a gray top bar / back button that badges views don't want (they have their own inline titles). - SectionItemView(click = { ModalManager.start.showCustomModal { close -> BadgesView(close) } }) { + SectionItemView(click = { ModalManager.start.showCustomModal { close -> BadgesView(ModalManager.start, close) } }) { val badgeType = chatModel.currentUser.value?.profile?.localBadge?.badge?.badgeType ?: BadgeType.Supporter Image(painterResource(badgeImage(badgeType)), stringResource(MR.strings.supporter_perks), Modifier.size(24.dp)) TextIconSpaced() diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 4a3f7870fe..cd4e0a89d1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -2762,7 +2762,6 @@ شارة غير متحقق منها لا تدعم خوادم ترحيل الدردشة المستعملة صفحات الويب. رمز صفحة الويب - يمكنك دعم SimpleX بدءًا من الإصدار 7 من التطبيق. خطأ في حفظ الاسم اسمح للناس بالتواصل معك عبر الاسم المسجَّل في عنوان SimpleX الخاص بك. اسمح للأشخاص بالانضمام باستخدام الاسم المسجَّل عبر رابط هذه القناة. 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 900897fc8a..42aecc9cfe 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -3134,7 +3134,6 @@ Runs in background to receive messages %s supports SimpleX Chat. %1$s supported SimpleX Chat. The badge expired on %2$s. - You can support SimpleX starting from v7 of the app. %s invested in SimpleX Chat crowdfunding. Unverified badge This badge could not be verified and may not be genuine. @@ -3176,6 +3175,7 @@ Support SimpleX Get badge + better files You can support SimpleX later in Settings. + Support SimpleX to send larger files that stay available longer Purchase successful Purchase pending The purchase is awaiting approval. This build does not deliver purchases approved later. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 6ea5d7686d..ccc7732a18 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -2858,7 +2858,6 @@ Abzeichen nicht verifiziert Die verwendeten Chat‑Relais unterstützen keine Webseiten. Webseiten-Code - Sie können SimpleX ab der App-Version v7 unterstützen. Fehler beim Speichern des Namens Lassen Sie sich über den mit Ihrer SimpleX‑Adresse registrierten Namen verbinden. Ermöglichen Sie Beitritte über den mit diesem Kanal‑Link registrierten Namen. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 59b99534cd..43e516561a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -2785,7 +2785,6 @@ Este grupo requiere una versión más reciente de la aplicación. Por favor, actualizala para unirte. Código web Los servidores usados no admiten páginas web. - Puedes apoyar SimpleX desde la versión 7. Insignia sin verificar Conectar con %s Error al guardar el nombre diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 51cfa90ff1..baa98ebf0b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -2833,7 +2833,6 @@ Fonctionne en arrière-plan pour recevoir les messages %s soutient SimpleX Chat. %1$s a soutenu SimpleX Chat. Le badge a expiré le %2$s. - Vous pouvez soutenir SimpleX à partir de la version 7 de l’application. %s a investi dans le financement participatif de SimpleX Chat. Badge non vérifié Ce badge n’a pas pu être vérifié et peut ne pas être authentique. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 8bc886865d..447dabcf3d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -2751,7 +2751,6 @@ visszaigazolt névsor %s támogatja a SimpleX Chatet. %1$s támogatta a SimpleX Chatet. A kitűző lejárt ekkor: %2$s. - A SimpleXet az alkalmazás v7-es verziójától kezdve támogathatja. %s befektetett a SimpleX Chat közösségi finanszírozásába. Ellenőrizetlen kitűző Nem sikerült ellenőrizni ezt a kitűzőt, és lehet, hogy nem eredeti. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index afaac45347..9b4e775685 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -2789,7 +2789,6 @@ Targhetta non verificata I relay di chat usati non supportano le pagine web. Codice pagina web - Puoi sostenere SimpleX dalla versione 7 dell\'app. Errore di salvataggio del nome Consenti alle persone di collegarsi tramite il nome registrato con il tuo indirizzo SimpleX. Consenti alle persone di entrare attraverso il nome registrato con questo link del canale. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index e5c425e7d7..0789cdcc69 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -2747,7 +2747,6 @@ メッセージを受信するためにバックグラウンドで実行します %s は SimpleX Chat をサポートしています。 %1$s は SimpleX Chat をサポートしました。バッジは %2$s に期限切れになりました。 - アプリのv7から SimpleX をサポートできます。 %s は SimpleX Chat のクラウドファンディングに出資しました。 未検証のバッジ このバッジは検証できず、本物ではない可能性があります。 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index 01239d6049..c353ad2a2f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -2316,7 +2316,6 @@ %1$d inscrito %1$d inscritos %1$s apoiou o SimpleX Chat. O selo expirou em %2$s. - Você pode apoiar o SimpleX a partir da versão 7 do aplicativo. Aceitar solicitação de contato Aceitar solicitação de contato aceito diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index ba571e4d0a..6f43e046d8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -2812,7 +2812,6 @@ Не удалось проверить подлинность этого значка. Возможно, он не является подлинным. Неподтвержденный значок %s инвестировал(а) в краудфандинг SimpleX Chat. - Вы можете поддержать SimpleX начиная с версии приложения v7. %s поддерживает SimpleX Chat. Работает в фоновом режиме для получения сообщений Нет серверов для разрешения имён. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 88148f022f..30414e244c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -2788,7 +2788,6 @@ sen Abonesiniz Bir bağlantı veya QR kodu paylaşabilirsiniz; böylece kanala herkes katılabilir. - Uygulamanın v7 sürümünden itibaren SimpleX\'i destekleyebilirsiniz. Bu aktarım bağlantısı (relay link) üzerinden kanala bağlandınız. Kanalınız Kanalınız diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 804316d18c..75e67cca37 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -2768,7 +2768,6 @@ 未验证的徽章 所用的聊天中继不支持网页。 网页代码 - 从 v7 版本起您可以支持 SimpleX。 高级选项 高级设置 允许任何人嵌入 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 357099ddef..5827cead70 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -2772,7 +2772,6 @@ 在背景執行以接收訊息 %s 支持 SimpleX Chat。 %1$s 曾支持 SimpleX Chat。此徽章已於 %2$s 過期。 - 你可以從應用程式 v7 開始支持 SimpleX。 %s 投資了 SimpleX Chat 眾籌。 未驗證徽章 無法驗證此徽章,可能並非真實。 diff --git a/apps/multiplatform/spec/client/chat-view.md b/apps/multiplatform/spec/client/chat-view.md index a6691c2878..adf8ad0224 100644 --- a/apps/multiplatform/spec/client/chat-view.md +++ b/apps/multiplatform/spec/client/chat-view.md @@ -271,7 +271,7 @@ Key sections: group profile, group link, member list with roles, group preferenc |---|---| | `ChatView.kt` | Main chat view, ChatLayout, ChatItemsList, ChatInfoToolbar | | `ChatInfoView.kt` | Contact info modal | -| `ChatItemInfoView.kt` | Individual message delivery/read info | +| `ChatItemInfoView.kt` | Individual message delivery/read info; below a file's expiry, for a user without a badge, a button opening the badges view | | `ChatItemsLoader.kt` | Pagination and message loading logic | | `ChatItemsMerger.kt` | MergedItems grouping of consecutive events | | `CommandsMenuView.kt` | Bot `/command` menu popup | From 692b0026e76898766c0c7b0fbd8240e1d207de46 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 23 Sep 2026 15:56:55 +0100 Subject: [PATCH 3/9] website: update cf texts (#7569) Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- blog/20260919-simplex-supporter-badges.md | 4 ++-- website/src/_includes/blog_previews/20260919.html | 2 +- website/src/crowdfunding.html | 11 +++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/blog/20260919-simplex-supporter-badges.md b/blog/20260919-simplex-supporter-badges.md index ebe19ef431..7aa2b2e4ac 100644 --- a/blog/20260919-simplex-supporter-badges.md +++ b/blog/20260919-simplex-supporter-badges.md @@ -55,10 +55,10 @@ Investors in our [equity crowdfunding on Wefunder](https://wefunder.com/simplex. | $2,500 | supporter, 12 months, or legend, 3 months | | $10,000 | legend, 12 months | -If you invest $500 or more by September 22, you will also receive [a public SimpleX name](https://simplex.domains?utm_source=blog) for 7 years[^name]. +If you invest $500 or more by November 22, you will also receive [a public SimpleX name](https://simplex.domains?utm_source=blog) for 5 years[^name]. Learn more and invest on Wefunder: [https://wefunder.com/simplex.chat](https://wefunder.com/simplex.chat?utm_source=blog) -[^name]: After September 22, investors of $500 or more receive a name for 5 years as early bird investors, and for 3 years after that — ahead of the public launch of names on December 12. +[^name]: Investors of $500 or more by September 22 receive a name for 7 years, and after November 22 for 3 years — ahead of the public launch of names on December 12. [^beta]: v7.1 beta is available via [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app) (Android beta), [TestFlight](https://testflight.apple.com/join/DWuT2LQu) (iOS), our [F-Droid repo](https://simplex.chat/fdroid/) and [GitHub](https://github.com/simplex-chat/simplex-chat/releases) (Android and desktop). diff --git a/website/src/_includes/blog_previews/20260919.html b/website/src/_includes/blog_previews/20260919.html index d6333c3c62..044c0d1d8b 100644 --- a/website/src/_includes/blog_previews/20260919.html +++ b/website/src/_includes/blog_previews/20260919.html @@ -2,4 +2,4 @@

Investors in our equity crowdfunding receive badges as perks.

-

If you invest $500 or more by September 22, you will also receive a public SimpleX name for 7 years. Learn more and invest on Wefunder.

+

If you invest $500 or more by November 22, you will also receive a public SimpleX name for 5 years. Learn more and invest on Wefunder.

diff --git a/website/src/crowdfunding.html b/website/src/crowdfunding.html index e7622a2ad6..32fdac1ba5 100644 --- a/website/src/crowdfunding.html +++ b/website/src/crowdfunding.html @@ -1231,7 +1231,7 @@ templateEngineOverride: njk
-

Invest $500+ by September 22 and get a SimpleX public name for your channel or business for 7 years, ahead of public launch.

+

Invest $500+ by November 22 during early bird terms and get a SimpleX public name for your channel or business for 5 years, ahead of public launch.

@@ -1397,9 +1397,10 @@ templateEngineOverride: njk var PHASES = [ { until: Date.UTC(2026, 8, 23, 12, 0, 0), what: 'SimpleX name for 7 years', terms: null }, { until: Date.UTC(2026, 10, 23, 12, 0, 0), what: 'SimpleX name for 5 years', - terms: 'Invest $500+ while early bird terms last.' }, + terms: 'Invest $500+ during early bird terms.', + wide: 'Invest $500+ during early bird terms, and get the name ahead of public launch.' }, { until: Infinity, what: 'SimpleX name for 5 years', - terms: 'Invest $500+ while early bird terms last.' } + terms: 'Invest $500+ during early bird terms.' } ]; var shown = -1; var digits = null; @@ -1525,6 +1526,7 @@ templateEngineOverride: njk if (leadShort) fitLine(leadShort); } var narrow = window.matchMedia('(max-width: 959px) and (orientation: portrait)'); + var desktop = window.matchMedia('(min-width: 960px)'); function left(ms) { var total = Math.floor(ms / 1000); var days = Math.floor(total / 86400); @@ -1541,7 +1543,8 @@ templateEngineOverride: njk if (changed) { shown = i; what.textContent = PHASES[i].what + (timed ? ':' : ''); - if (PHASES[i].terms !== null) terms.textContent = PHASES[i].terms; + var line = desktop.matches && PHASES[i].wide ? PHASES[i].wide : PHASES[i].terms; + if (line !== null) terms.textContent = line; if (!timed) { clock.textContent = ''; clearInterval(timer); fit(); return; } } if (!digits) { From 23136f1cd5c60be8b70325ec382913782aa26a5b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 23 Sep 2026 21:29:41 +0100 Subject: [PATCH 4/9] core: update agent API for connections (#7574) * core: update agent API for connections * update simplexmq --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Library/Commands.hs | 16 ++++++++-------- src/Simplex/Chat/Library/Internal.hs | 10 +++++----- src/Simplex/Chat/Library/Subscriber.hs | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cabal.project b/cabal.project index 5e00d0764b..b8c73b14cf 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 900c45ffaee5eb7eef8ec9402bc4971e4eb7ef33 + tag: 5294b7d8b7285a887b849b738e29bec207e5dde8 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 926d6e8860..a77e1d0c8c 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."900c45ffaee5eb7eef8ec9402bc4971e4eb7ef33" = "1iygb9hkc86ky553jg5apc8vfbjj0lghjaywikc0m7h1yk7zibxg"; + "https://github.com/simplex-chat/simplexmq.git"."5294b7d8b7285a887b849b738e29bec207e5dde8" = "0adjvg6mcyk936qzyl3a4daq68kslzpqhpdh369cf8wwcfqkiqg4"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 91ef260bfa..7e67dc5b68 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1721,7 +1721,7 @@ processChatCommand cxt nm = \case Just _ -> do let chatV = initialChatVersion subMode <- chatReadVar subscriptionMode - connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff + (connId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff conn@Connection {connId = testCId} <- withFastStore $ \db -> createRelayTestConnection db cxt user connId ConnPrepared chatV subMode challenge <- drgRandomBytes 32 @@ -2844,7 +2844,7 @@ processChatCommand cxt nm = \case dm <- encodeConnInfo $ XGrpAcpt membershipMemId (Just $ groupMemberKey gks) agentConnId <- case memberConn fromMember of Nothing -> do - agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff + (agentConnId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff let chatV = vr cxt `peerConnChatVersion` peerChatVRange void $ withFastStore' $ \db -> createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode pure agentConnId @@ -3398,7 +3398,7 @@ processChatCommand cxt nm = \case -- possible improvement: use agent connRequestAgentVersion to determine pqSupport here; -- for joinPreparedConn below - same + encodeConnInfoPQ; -- same for auto-accept on xGrpDirectInv - acId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff + (acId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff conn <- withStore $ \db -> do connId <- liftIO $ createMemberContactConn db user acId Nothing gInfo mConn ConnPrepared contactId subMode getConnectionById db cxt user connId @@ -3757,7 +3757,7 @@ processChatCommand cxt nm = \case withMemberName gName mName cmd = withUser $ \user -> getGroupAndMemberId user gName mName >>= processChatCommand cxt nm . uncurry cmd getConnectionCode :: ConnId -> CM Text - getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId) + getConnectionCode connId = verificationCode . codeAD <$> withAgent (`getConnectionVerifyCodes` connId) getChannelMemberCode :: GroupInfo -> GroupMember -> CM Text getChannelMemberCode GroupInfo {membership} m = case (memberPubKey membership, memberPubKey m) of @@ -3818,7 +3818,7 @@ processChatCommand cxt nm = \case joinNewConn chatV = do -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' + (connId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ conn <- withFastStore' $ \db -> createDirectConnection' db userId connId ccLink contactId_ ConnPrepared incognitoProfile subMode chatV pqSup' joinPreparedConn conn incognitoProfile @@ -3965,7 +3965,7 @@ processChatCommand cxt nm = \case Nothing -> throwChatError CEInvalidConnReq Just _ -> do let chatV = initialChatVersion - connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup + (connId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup pure (connId, chatV) mkXContactId :: Maybe XContactId -> CM XContactId mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure @@ -4304,7 +4304,7 @@ processChatCommand cxt nm = \case let chatV = initialChatVersion gVar <- asks random subMode <- chatReadVar subscriptionMode - connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff + (connId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff (relayMember, conn, groupRelay) <- withFastStore $ \db -> do relayMember <- createRelayForOwner db cxt gVar user gInfo relay groupRelay <- createGroupRelayRecord db gInfo relayMember relay @@ -4707,7 +4707,7 @@ processChatCommand cxt nm = \case SRDirect contactId -> do ct <- withFastStore $ \db -> getContact db cxt u contactId forM (contactConn ct) $ \conn -> - (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) + (CBDirect,) . codeAD <$> withAgent (`getConnectionVerifyCodes` aConnId conn) SRGroup toGroupId _ asGroup -> do GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db cxt u toGroupId pure $ mkBinding m <$> publicGroup diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index e40e2466a5..f6c1fb3b9e 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -977,7 +977,7 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId (ct, conn, incognitoProfile) <- case contactId_ of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' + (connId, _) <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' (ct, conn) <- withStore' $ \db -> createContactFromRequest db user userContactLinkId_ connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False pure (ct, conn, incognitoProfile) @@ -986,7 +986,7 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId case contactConn ct of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' + (connId, _) <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' currentTs <- liftIO getCurrentTime conn <- withStore' $ \db -> do forM_ xContactId $ \xcId -> setContactAcceptedXContactId db ct xcId @@ -2328,7 +2328,7 @@ type HistoryFile = (FileInvitation, RcvFileDescrText, Maybe UTCTime, Maybe Badge directChatBinding :: Contact -> CM (Maybe ByteString) directChatBinding ct = forM (contactConn ct) $ \conn -> - encodeChatBinding CBDirect <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) + encodeChatBinding CBDirect . codeAD <$> withAgent (`getConnectionVerifyCodes` aConnId conn) rcvGroupChatBinding :: GroupInfo -> Maybe GroupMember -> ShowGroupAsSender -> Maybe BadgeProof -> Maybe ByteString rcvGroupChatBinding gInfo m_ asGroup badge_ = @@ -3028,7 +3028,7 @@ prepareAgentJoin user conn_ enableNtfs cReqUri = do cmdId <- withStore' $ \db -> createCommand db user (dbConnId <$> conn_) CFJoinConn connId <- case conn_ of Just conn -> pure $ aConnId conn - Nothing -> withAgent $ \a -> prepareConnectionToJoin a (aUserId user) enableNtfs cReqUri PQSupportOff + Nothing -> fst <$> withAgent (\a -> prepareConnectionToJoin a (aUserId user) enableNtfs cReqUri PQSupportOff) pure (cmdId, connId) joinAgentConnectionAsync :: CommandId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM () @@ -3054,7 +3054,7 @@ allowAgentConnectionInfo user conn@Connection {connId} confId dm = do prepareAgentAccept :: User -> Bool -> InvitationId -> PQSupport -> CM (CommandId, ConnId) prepareAgentAccept user enableNtfs invId pqSup = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact - connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) enableNtfs invId pqSup + (connId, _) <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) enableNtfs invId pqSup pure (cmdId, connId) agentAcceptContactAsync :: MsgEncodingI e => CommandId -> ConnId -> Bool -> InvitationId -> ChatMsgEvent e -> PQSupport -> SubscriptionMode -> CM () diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 2288763791..1dbf1adcb9 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1390,7 +1390,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag processContactConnMessage :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () processContactConnMessage agentMsg connEntity conn UserContact {userContactLinkId = uclId, groupId = ucGroupId_} = case agentMsg of - REQ invId pqSupport _ connInfo rejectionSupported -> do + REQ invId pqSupport _ connInfo _ rejectionSupported -> do (signedMsg_, ChatMessage {chatVRange, chatMsgEvent}) <- parseChatMessage' conn connInfo case chatMsgEvent of XContact p memberKey_ xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p memberKey_ xContactId_ welcomeMsgId_ requestMsg_ pqSupport rejectionSupported @@ -1908,7 +1908,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag keepSig <- case contactConn ct of Nothing -> pure False Just conn -> do - adHash <- withAgent (`getConnectionRatchetAdHash` aConnId conn) + adHash <- codeAD <$> withAgent (`getConnectionVerifyCodes` aConnId conn) pure $ encodeChatBinding CBDirect adHash == binding pure $ if keepSig then c else MCChat {text, chatLink, ownerSig = Nothing} _ -> pure c @@ -3877,7 +3877,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag securityCodeChanged mCt' createItems mCt' m | otherwise = do - acId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connReq PQSupportOff + (acId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connReq PQSupportOff mCt' <- withStore $ \db -> do updateMemberContactInvited db user mCt groupDirectInv void $ liftIO $ createMemberContactConn db user acId Nothing g mConn ConnPrepared mContactId subMode @@ -3898,7 +3898,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) createItems mCt m' | otherwise = do - acId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connReq PQSupportOff + (acId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connReq PQSupportOff (mCt, m') <- withStore $ \db -> do (mContactId, m') <- liftIO $ createMemberContactInvited db user g m groupDirectInv void $ liftIO $ createMemberContactConn db user acId Nothing g mConn ConnPrepared mContactId subMode From 3af6e2abfe63b97ac64066fc0b0d98eca9052dfd Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 23 Sep 2026 23:25:20 +0100 Subject: [PATCH 5/9] core: store extended contact and group preferences, for forward compatibility (#7506) * core: store extended contact and group preferences, for forward compatibility * simiplify * fix * fix bot api types * query plans --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- bots/src/API/Docs/Types.hs | 20 ++- simplex-chat.cabal | 2 + src/Simplex/Chat/Store/Connections.hs | 14 +- src/Simplex/Chat/Store/ContactRequest.hs | 8 +- src/Simplex/Chat/Store/Direct.hs | 13 +- src/Simplex/Chat/Store/Groups.hs | 61 ++++---- src/Simplex/Chat/Store/Messages.hs | 10 +- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20260923_preferences_json.hs | 21 +++ .../Store/Postgres/Migrations/chat_schema.sql | 6 +- src/Simplex/Chat/Store/Profiles.hs | 11 +- src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20260923_preferences_json.hs | 20 +++ .../SQLite/Migrations/chat_query_plans.txt | 143 +++++++++--------- .../Store/SQLite/Migrations/chat_schema.sql | 6 +- src/Simplex/Chat/Store/Shared.hs | 36 ++--- src/Simplex/Chat/Types/Preferences.hs | 85 +++++++++-- tests/ProtocolTests.hs | 41 ++++- 18 files changed, 334 insertions(+), 171 deletions(-) create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20260923_preferences_json.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20260923_preferences_json.hs diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 792f3e6fd5..ac5992770e 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -20,7 +20,7 @@ import Data.Char (isUpper, toLower, toUpper) import Data.List (find, mapAccumL, sortOn) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, mapMaybe) import qualified Data.Set as S import Data.Text (Text) import GHC.Generics @@ -177,11 +177,15 @@ normalizeConsName pfx consName -- making chatDir optional because clients use CIDirection? instead of CIQDirection (the type is replaced in Types.hs) ciQuoteType :: SumTypeInfo -ciQuoteType = - let st@(STI _ records) = sti @(CIQuote 'CTDirect) - optChatDir f@(FieldInfo n t) = if n == "chatDir" then FieldInfo n (TIOptional t) else f - updateRecord (RecordTypeInfo name fields) = RecordTypeInfo name $ map optChatDir fields - in st {recordTypes = map updateRecord records} -- need to map even though there is one constructor in this type +ciQuoteType = updateFields mkOptional $ sti @(CIQuote 'CTDirect) + where + mkOptional = map (\f@(FieldInfo n t) -> if n == "chatDir" then FieldInfo n (TIOptional t) else f) + +removeField :: String -> SumTypeInfo -> SumTypeInfo +removeField n = updateFields $ mapMaybe (\f@(FieldInfo n' _) -> if n == n' then Nothing else Just f) + +updateFields :: ([FieldInfo] -> [FieldInfo]) -> SumTypeInfo -> SumTypeInfo +updateFields f st@(STI _ records) = st {recordTypes = map (\(RecordTypeInfo name fields) -> RecordTypeInfo name $ f fields) records} -- type info, JSON encoding, constructor prefix, removed constructors, string encoding for commands, description chatTypesDocsData :: [(SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text)] @@ -302,7 +306,7 @@ chatTypesDocsData = (sti @GroupMemberSettings, STRecord, "", [], "", ""), (sti @GroupMemberStatus, STEnum' ((\case "group_deleted" -> "deleted"; "intro_invited" -> "intro-inv"; s -> s) . consSep "GSMem" '_'), "", [], "", ""), (sti @GroupPreference, STRecord, "", [], "", ""), - (sti @GroupPreferences, STRecord, "", [], "", ""), + (removeField "_json" $ sti @GroupPreferences, STRecord, "", [], "", ""), (sti @GroupProfile, STRecord, "", [], "", ""), (sti @GroupRelay, STRecord, "", [], "", ""), (sti @GroupShortLinkData, STRecord, "", [], "", ""), @@ -338,7 +342,7 @@ chatTypesDocsData = (sti @PendingContactConnection, STRecord, "", [], "", ""), (sti @PlanResolveMode, STEnum, "PRM", [], "", ""), (sti @PrefEnabled, STRecord, "", [], "", ""), - (sti @Preferences, STRecord, "", [], "", ""), + (removeField "_json" $ sti @Preferences, STRecord, "", [], "", ""), (sti @PreparedContact, STRecord, "", [], "", ""), (sti @GroupDirectInvitation, STRecord, "", [], "", ""), (sti @PreparedGroup, STRecord, "", [], "", ""), diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 72f162d7d8..153bef02ea 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -167,6 +167,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260904_file_badges Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges Simplex.Chat.Store.Postgres.Migrations.M20260918_badge_issue_errors + Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json else exposed-modules: Simplex.Chat.Archive @@ -343,6 +344,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260904_file_badges Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges Simplex.Chat.Store.SQLite.Migrations.M20260918_badge_issue_errors + Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 1057a71a02..0867118cb6 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -37,6 +37,7 @@ import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Shared import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (ConnId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', fromOnlyBI, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) @@ -119,7 +120,7 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d [sql| SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, cr2.rejection_supported, + p.preferences, p.preferences_json, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, cr2.rejection_supported, c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, @@ -131,8 +132,9 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d |] (userId, contactId, CSActive) toContact' :: UTCTime -> Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact - toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) = - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} + toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) = + let preferences = chatPrefsFromRow encodedPrefs receivedPrefs + profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn @@ -153,7 +155,7 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, @@ -164,13 +166,13 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.preferences_json, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index f87a1e242a..5ef9cbe723 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -116,7 +116,7 @@ createOrUpdateContactRequest SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, + cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, @@ -153,7 +153,7 @@ createOrUpdateContactRequest cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified @@ -172,8 +172,8 @@ createOrUpdateContactRequest liftIO $ DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, description, image, contactLink, userId) :. ("" :: LocalAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, local_alias, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId) :. ("" :: LocalAlias, currentTs, currentTs) :. badgeToRow badge badgeVerified :. prefsToRow preferences) profileId <- liftIO $ insertedRowId db liftIO $ DB.execute diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index a87d266b03..0e68221056 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -322,7 +322,7 @@ getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, + cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, @@ -737,12 +737,13 @@ updateContactProfile_' db userId profileId Profile {displayName, fullName, short db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, + SET preferences = ?, preferences_json = ?, + display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, chat_peer_type = ?, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) + (prefsToRow preferences :. (displayName, fullName, shortDescr, description, image, contactLink, peerType, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) -- update only member profile fields (when member doesn't have associated contact - we can reset contactLink and prefs) updateMemberContactProfileReset_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO () @@ -756,7 +757,7 @@ updateMemberContactProfileReset_' db userId profileId Profile {displayName, full db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = NULL, preferences = NULL, preferences_json = NULL, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? @@ -851,7 +852,7 @@ contactRequestQuery = cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, @@ -973,7 +974,7 @@ getContact_ db cxt user@User {userId} contactId deleted = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, + cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 2c0b2dc080..f9cb76782a 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -216,7 +216,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) -import Data.Bifunctor (first, second) +import Data.Bifunctor (first) import Data.ByteString (ByteString) import qualified Data.ByteString as B import Data.Char (toLower) @@ -263,11 +263,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime) +type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Text, Maybe Text) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime) toMaybeGroupMember :: UTCTime -> Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, description, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) = - Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) +toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, description, image, contactLink, peerType, Just localAlias, encodedPrefs, receivedPrefs) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) = + Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, encodedPrefs, receivedPrefs) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) toMaybeGroupMember _ _ _ = Nothing createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink @@ -403,11 +403,11 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile memberId (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, - user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + user_id, member_admission, created_at, updated_at, preferences, preferences_json) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup - :. (userId, groupPreferences, memberAdmission, currentTs, currentTs)) + :. (userId, memberAdmission, currentTs, currentTs) :. prefsToRow groupPreferences) profileId <- insertedRowId db DB.execute db @@ -486,8 +486,8 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti groupId <- liftIO $ do DB.execute db - "INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" - (displayName, fullName, shortDescr, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) + "INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, user_id, member_admission, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, userId, memberAdmission, currentTs, currentTs) :. prefsToRow groupPreferences) profileId <- insertedRowId db DB.execute db @@ -917,11 +917,11 @@ createGroup_ db userId groupProfile prepared business useRelays relayOwnStatus p (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, - user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + user_id, member_admission, created_at, updated_at, preferences, preferences_json) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup - :. (userId, groupPreferences, memberAdmission, currentTs, currentTs)) + :. (userId, memberAdmission, currentTs, currentTs) :. prefsToRow groupPreferences) profileId <- insertedRowId db DB.execute db @@ -1071,11 +1071,11 @@ getBaseGroupDetails db cxt User {userId, userContactId} _contactId_ search_ = do getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [(GroupMemberRole, FullGroupPreferences)] getContactGroupPreferences db User {userId} Contact {contactId} = do - map (second mergeGroupPreferences) + map (\(role, encodedPrefs, receivedPrefs) -> (role, mergeGroupPreferences $ groupPrefsFromRow encodedPrefs receivedPrefs)) <$> DB.query db [sql| - SELECT m.member_role, gp.preferences + SELECT m.member_role, gp.preferences, gp.preferences_json FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members m USING (group_id) @@ -2087,8 +2087,8 @@ createJoiningMember liftIO $ DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, description, image, contactLink, userId, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId, currentTs, currentTs) :. badgeToRow badge badgeVerified :. prefsToRow preferences) profileId <- liftIO $ insertedRowId db case cReqMemberId_ of Just memberId -> do @@ -2169,8 +2169,8 @@ createBusinessRequestGroup liftIO $ DB.execute db - "INSERT INTO group_profiles (display_name, full_name, short_descr, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, shortDescr, image, userId, groupPreferences, currentTs, currentTs) + "INSERT INTO group_profiles (display_name, full_name, short_descr, image, user_id, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, image, userId, currentTs, currentTs) :. prefsToRow (Just groupPreferences)) groupProfileId <- liftIO $ insertedRowId db liftIO $ DB.execute @@ -2440,8 +2440,8 @@ createNewMemberProfile_ db cxt User {userId} Profile {displayName, fullName, sho badgeVerified <- verifyBadge_ (badgeKeys cxt) badge DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, description, image, contactLink, userId, preferences, createdAt, createdAt) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId, createdAt, createdAt) :. badgeToRow badge badgeVerified :. prefsToRow preferences) profileId <- insertedRowId db pure $ Right (ldn, profileId, badgeVerified) @@ -2719,19 +2719,21 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, db [sql| UPDATE group_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, + SET preferences = ?, preferences_json = ?, + display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, group_type = ?, group_link = ?, group_web_page = ?, group_domain = CASE WHEN ? THEN ? ELSE group_domain END, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?, - preferences = ?, member_admission = ?, updated_at = ? + member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ? ) |] - ( (newName, fullName, shortDescr, description, image, groupType_, groupLink_) + ( prefsToRow groupPreferences + :. (newName, fullName, shortDescr, description, image, groupType_, groupLink_) :. (groupWebPage_, isJust publicGroup, groupDomain_, domainWebPage_, allowEmbedding_, groupDomainProof_) - :. (groupPreferences, memberAdmission, currentTs, userId, groupId) + :. (memberAdmission, currentTs, userId, groupId) ) updateGroup_ ldn currentTs = do DB.execute @@ -2768,14 +2770,14 @@ updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} db [sql| UPDATE group_profiles - SET preferences = ?, updated_at = ? + SET preferences = ?, preferences_json = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ? ) |] - (ps, currentTs, userId, groupId) + (prefsToRow (Just ps) :. (currentTs, userId, groupId)) pure (g :: GroupInfo) {groupProfile = p {groupPreferences = Just ps}, fullGroupPreferences = mergeGroupPreferences $ Just ps} updateGroupProfileFromMember :: DB.Connection -> User -> GroupInfo -> Profile -> ExceptT StoreError IO GroupInfo @@ -2795,15 +2797,16 @@ getGroupProfileById db groupId = SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, - gp.preferences, gp.member_admission + gp.preferences, gp.preferences_json, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id WHERE g.group_id = ? |] (Only groupId) where - toGroupProfile ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (groupPreferences, memberAdmission)) = - let publicGroupAccess = toPublicGroupAccess accessRow + toGroupProfile ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (encodedPrefs, receivedPrefs, memberAdmission)) = + let groupPreferences = groupPrefsFromRow encodedPrefs receivedPrefs + publicGroupAccess = toPublicGroupAccess accessRow in GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ publicGroupAccess, groupPreferences, memberAdmission} getGroupInfoByUserContactLinkConnReq :: DB.Connection -> StoreCxt -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 3998e1f249..b8909da1b3 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -725,7 +725,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at @@ -1148,7 +1148,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = do cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified @@ -3110,7 +3110,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -3119,14 +3119,14 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do -- quoted GroupMember rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, - rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, + rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rp.preferences_json, rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified, rm.created_at, rm.updated_at, rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, + dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbp.preferences_json, dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified, dbm.created_at, dbm.updated_at, dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link, dbm.member_security_code, dbm.member_security_code_verified_at diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 40c13527d9..24ed45c9a9 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -52,6 +52,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260828_file_expiry import Simplex.Chat.Store.Postgres.Migrations.M20260904_file_badges import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges import Simplex.Chat.Store.Postgres.Migrations.M20260918_badge_issue_errors +import Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -103,7 +104,8 @@ schemaMigrations = ("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry), ("20260904_file_badges", m20260904_file_badges, Just down_m20260904_file_badges), ("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges), - ("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors) + ("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors), + ("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260923_preferences_json.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260923_preferences_json.hs new file mode 100644 index 0000000000..81039139e8 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260923_preferences_json.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260923_preferences_json :: Text +m20260923_preferences_json = + [r| +ALTER TABLE contact_profiles ADD COLUMN preferences_json TEXT; +ALTER TABLE group_profiles ADD COLUMN preferences_json TEXT; +|] + +down_m20260923_preferences_json :: Text +down_m20260923_preferences_json = + [r| +ALTER TABLE group_profiles DROP COLUMN preferences_json; +ALTER TABLE contact_profiles DROP COLUMN preferences_json; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 0c7bf85cb6..ad975874db 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -680,7 +680,8 @@ CREATE TABLE test_chat_schema.contact_profiles ( contact_domain text, contact_domain_proof text, contact_domain_verified smallint, - description text + description text, + preferences_json text ); @@ -1042,7 +1043,8 @@ CREATE TABLE test_chat_schema.group_profiles ( group_domain text, domain_web_page bigint, allow_embedding bigint, - group_domain_proof text + group_domain_proof text, + preferences_json text ); diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 20e05009a5..6498279f77 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -375,10 +375,11 @@ updateUserProfileFields_' db userId profileId Profile {displayName, fullName, sh db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? + SET preferences = ?, preferences_json = ?, + display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, chat_peer_type = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, updatedAt) :. (userId, profileId)) + (prefsToRow preferences :. (displayName, fullName, shortDescr, description, image, contactLink, peerType, updatedAt) :. (userId, profileId)) -- store the user's own badge credential; touches only the badge columns. -- bumps user_member_profile_updated_at so groups receive the updated profile (with the badge) on the next message. @@ -429,14 +430,14 @@ getUserContactProfiles db User {userId} = <$> DB.query db [sql| - SELECT display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, contact_domain, preferences + SELECT display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, contact_domain, preferences, preferences_json FROM contact_profiles WHERE user_id = ? |] (Only userId) where - toContactProfile :: (ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe SimplexDomain, Maybe Preferences) -> Profile - toContactProfile (displayName, fullName, shortDescr, description, image, contactLink, peerType, domain_, preferences) = Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain = mkDomainClaim <$> domain_, peerType, preferences, badge = Nothing} + toContactProfile :: (ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe SimplexDomain, Maybe Text, Maybe Text) -> Profile + toContactProfile (displayName, fullName, shortDescr, description, image, contactLink, peerType, domain_, encodedPrefs, receivedPrefs) = Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain = mkDomainClaim <$> domain_, peerType, preferences = chatPrefsFromRow encodedPrefs receivedPrefs, badge = Nothing} createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> C.PrivateKeyEd25519 -> ExceptT StoreError IO () createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode linkPrivSigKey = diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index f49495168e..4dcefa1598 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -175,6 +175,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260828_file_expiry import Simplex.Chat.Store.SQLite.Migrations.M20260904_file_badges import Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges import Simplex.Chat.Store.SQLite.Migrations.M20260918_badge_issue_errors +import Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -349,7 +350,8 @@ schemaMigrations = ("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry), ("20260904_file_badges", m20260904_file_badges, Just down_m20260904_file_badges), ("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges), - ("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors) + ("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors), + ("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260923_preferences_json.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260923_preferences_json.hs new file mode 100644 index 0000000000..4ee4dbc1e2 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260923_preferences_json.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260923_preferences_json :: Query +m20260923_preferences_json = + [sql| +ALTER TABLE contact_profiles ADD COLUMN preferences_json TEXT; +ALTER TABLE group_profiles ADD COLUMN preferences_json TEXT; +|] + +down_m20260923_preferences_json :: Query +down_m20260923_preferences_json = + [sql| +ALTER TABLE group_profiles DROP COLUMN preferences_json; +ALTER TABLE contact_profiles DROP COLUMN preferences_json; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 4062a1e8f6..838a172a15 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -124,7 +124,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, + cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, @@ -149,7 +149,7 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, @@ -160,13 +160,13 @@ Query: mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.preferences_json, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at @@ -405,7 +405,7 @@ Query: cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified @@ -528,10 +528,11 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE group_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, + SET preferences = ?, preferences_json = ?, + display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, group_type = ?, group_link = ?, group_web_page = ?, group_domain = CASE WHEN ? THEN ? ELSE group_domain END, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?, - preferences = ?, member_admission = ?, updated_at = ? + member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups @@ -733,7 +734,7 @@ Plan: Query: SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, cr2.rejection_supported, + p.preferences, p.preferences_json, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, cr2.rejection_supported, c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, @@ -1053,7 +1054,7 @@ Query: -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at @@ -1327,8 +1328,8 @@ Query: (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, - user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + user_id, member_admission, created_at, updated_at, preferences, preferences_json) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1405,7 +1406,7 @@ Query: -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -1414,14 +1415,14 @@ Query: -- quoted GroupMember rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, - rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, + rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rp.preferences_json, rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified, rm.created_at, rm.updated_at, rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, + dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbp.preferences_json, dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified, dbm.created_at, dbm.updated_at, dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link, dbm.member_security_code, dbm.member_security_code_verified_at @@ -1473,7 +1474,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, + cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, @@ -2065,7 +2066,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, + cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, @@ -2154,7 +2155,7 @@ Query: cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified @@ -2184,7 +2185,7 @@ Query: cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified @@ -2214,7 +2215,7 @@ Query: cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified @@ -3765,7 +3766,7 @@ Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences + SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, cp.preferences_json, -- , ct.user_preferences cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified FROM contact_profiles cp WHERE cp.user_id = ? AND cp.contact_profile_id = ? @@ -3828,7 +3829,7 @@ SEARCH f USING PRIMARY KEY (file_id=?) SEARCH d USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, contact_domain, preferences + SELECT display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, contact_domain, preferences, preferences_json FROM contact_profiles WHERE user_id = ? @@ -3949,7 +3950,7 @@ Query: SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, - gp.preferences, gp.member_admission + gp.preferences, gp.preferences_json, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id WHERE g.group_id = ? @@ -5347,25 +5348,7 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? - WHERE user_id = ? AND contact_profile_id = ? - -Plan: -SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) - -Query: - UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, - contact_domain = ?, contact_domain_proof = ? - WHERE user_id = ? AND contact_profile_id = ? - -Plan: -SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) - -Query: - UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = NULL, preferences = NULL, preferences_json = NULL, updated_at = ?, badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? @@ -5391,6 +5374,26 @@ Query: Plan: SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE contact_profiles + SET preferences = ?, preferences_json = ?, + display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, chat_peer_type = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_profiles + SET preferences = ?, preferences_json = ?, + display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, chat_peer_type = ?, updated_at = ?, + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? + WHERE user_id = ? AND contact_profile_id = ? + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contact_profiles SET contact_domain_verified = ? WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?) @@ -5503,7 +5506,7 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE group_profiles - SET preferences = ?, updated_at = ? + SET preferences = ?, preferences_json = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups @@ -5737,7 +5740,7 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, @@ -5747,7 +5750,7 @@ Query: -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.preferences_json, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at @@ -5775,7 +5778,7 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, @@ -5785,7 +5788,7 @@ Query: -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.preferences_json, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at @@ -5806,7 +5809,7 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, @@ -5816,7 +5819,7 @@ Query: -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.preferences_json, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at @@ -5837,7 +5840,7 @@ Query: cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, @@ -5854,7 +5857,7 @@ Query: cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, p.preferences_json, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, @@ -5869,7 +5872,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5897,7 +5900,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5918,7 +5921,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5938,7 +5941,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5958,7 +5961,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5978,7 +5981,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -5998,7 +6001,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6018,7 +6021,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6038,7 +6041,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6058,7 +6061,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6078,7 +6081,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6098,7 +6101,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6118,7 +6121,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6138,7 +6141,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -6158,7 +6161,7 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -7157,15 +7160,15 @@ Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, descr Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) -Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, user_id, local_alias, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) -Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) -Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, local_alias, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) @@ -7204,10 +7207,10 @@ Plan: Query: INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, roster_transfer_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: -Query: INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) +Query: INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, user_id, member_admission, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?) Plan: -Query: INSERT INTO group_profiles (display_name, full_name, short_descr, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?) +Query: INSERT INTO group_profiles (display_name, full_name, short_descr, image, user_id, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?) Plan: Query: INSERT INTO group_snd_item_statuses (chat_item_id, group_member_id, group_snd_item_status) VALUES (?,?,?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index b6eddbaf0f..995497d5ee 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -32,7 +32,8 @@ CREATE TABLE contact_profiles( contact_domain TEXT, contact_domain_proof TEXT, contact_domain_verified INTEGER, - description TEXT + description TEXT, + preferences_json TEXT ) STRICT; CREATE TABLE users( user_id INTEGER PRIMARY KEY, @@ -146,7 +147,8 @@ CREATE TABLE group_profiles( group_domain TEXT, domain_web_page INTEGER, allow_embedding INTEGER, - group_domain_proof TEXT + group_domain_proof TEXT, + preferences_json TEXT ) STRICT; CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 8eeefa328f..d04a484232 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -421,8 +421,8 @@ createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, badgeVerified <- verifyBadge_ (badgeKeys cxt) badge DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, description, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, user_id, local_alias, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, peerType) :. (userId, localAlias, currentTs, currentTs) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. prefsToRow preferences) profileId <- insertedRowId db DB.execute db @@ -489,15 +489,16 @@ type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Ma type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt) -type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe BoolInt, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow +type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe BoolInt, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow type ContactRow = Only ContactId :. ContactRow' type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe BoolInt) toContact :: UTCTime -> StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact -toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} +toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) = + let preferences = chatPrefsFromRow encodedPrefs receivedPrefs + profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} activeConn = toMaybeConnection cxt connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn @@ -539,18 +540,18 @@ getProfileById db userId profileId = do DB.query db [sql| - SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences + SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, cp.preferences_json, -- , ct.user_preferences cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified FROM contact_profiles cp WHERE cp.user_id = ? AND cp.contact_profile_id = ? |] (userId, profileId) -type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64, BoolInt) :. (Int64, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64, BoolInt) :. (Int64, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Text, Maybe Text, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow toContactRequest :: UTCTime -> ContactRequestRow -> UserContactRequest -toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, BI rejectionSupported) :. (profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias} +toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, BI rejectionSupported) :. (profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, encodedPrefs, receivedPrefs, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, preferences = chatPrefsFromRow encodedPrefs receivedPrefs, localBadge = rowToBadge now badgeRow, localAlias} cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt, rejectionSupported} @@ -686,18 +687,19 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof) type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime) -type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow +type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Text, Maybe Text) :. BadgeRow :. ContactDomainRow toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> (GroupInfo, GroupKeysRow) -toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) = +toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) = let membership = (toGroupMember now userContactId userMemberRow) {memberChatVRange = vr cxt} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} + groupPreferences = groupPrefsFromRow encodedPrefs receivedPrefs fullGroupPreferences = mergeGroupPreferences groupPreferences publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ (toPublicGroupAccess accessRow) groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} @@ -793,7 +795,7 @@ groupMemberQuery = [sql| SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, @@ -811,8 +813,8 @@ toContactMember now cxt User {userContactId} (memberRow :. connRow) = (toGroupMember now userContactId memberRow) {activeConn = toMaybeConnection cxt connRow} rowToLocalProfile :: UTCTime -> ProfileRow -> LocalProfile -rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, preferences) :. badgeRow :. domainRow) = - LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences} +rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, encodedPrefs, receivedPrefs) :. badgeRow :. domainRow) = + LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences = chatPrefsFromRow encodedPrefs receivedPrefs} toBusinessChatInfo :: Maybe SimplexDomainClaim -> BusinessChatInfoRow -> Maybe BusinessChatInfo toBusinessChatInfo businessDomain (Just chatType, Just businessId, Just customerId) = Just BusinessChatInfo {chatType, businessId, customerId, businessDomain} @@ -828,7 +830,7 @@ groupInfoQueryFields = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, - g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, @@ -838,7 +840,7 @@ groupInfoQueryFields = -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, pu.preferences_json, pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index c26f187343..7445a9c2d5 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -24,8 +24,10 @@ module Simplex.Chat.Types.Preferences where import Control.Applicative ((<|>)) -import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Aeson (FromJSON (..), Object, ToJSON (..), Value (..), decodeStrictText) +import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as J +import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import Data.Maybe (fromMaybe, isJust) @@ -37,7 +39,7 @@ import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON, taggedObjectJSON) -import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (encodeJSON, safeDecodeUtf8, (<$?>)) data ChatFeature = CFTimedMessages @@ -149,6 +151,34 @@ setPreference_ f pref_ prefs = SCFCalls -> prefs {calls = pref_} SCFSessions -> prefs {sessions = pref_} +newtype PrefsJSON = PrefsJSON {unPrefsJSON :: Maybe Object} + deriving (Eq, Show) + +instance ToJSON PrefsJSON where + toJSON _ = Null + toEncoding _ = JE.null_ + omitField _ = True + +instance FromJSON PrefsJSON where + parseJSON _ = pure $ PrefsJSON Nothing + omittedField = Just $ PrefsJSON Nothing + +keepPrefsJSON :: (ToJSON p, HasField "_json" p PrefsJSON) => Value -> p -> p +keepPrefsJSON v ps = setField @"_json" ps . PrefsJSON $ case v of + Object o | v /= toJSON ps -> Just o + _ -> Nothing + +decodePrefs :: (Value -> JT.Parser p) -> Text -> Maybe p +decodePrefs prefsP t = JT.parseMaybe prefsP =<< decodeStrictText t + +prefsFromRow_ :: (Value -> JT.Parser p) -> Maybe Text -> Maybe Text -> Maybe p +prefsFromRow_ prefsP encodedPrefs receivedPrefs = (decode =<< receivedPrefs) <|> (decode =<< encodedPrefs) + where + decode = decodePrefs prefsP + +prefsToRow :: HasField "_json" p PrefsJSON => Maybe p -> (Maybe p, Maybe Text) +prefsToRow ps = (ps, encodeJSON . Object <$> (unPrefsJSON . getField @"_json" =<< ps)) + -- collection of optional chat preferences for the user and the contact data Preferences = Preferences { timedMessages :: Maybe TimedMessagesPreference, @@ -158,7 +188,8 @@ data Preferences = Preferences files :: Maybe FilesPreference, calls :: Maybe CallsPreference, sessions :: Maybe SessionsPreference, - commands :: Maybe [ChatBotCommand] + commands :: Maybe [ChatBotCommand], + _json :: PrefsJSON } deriving (Eq, Show) @@ -166,6 +197,9 @@ class HasCommands p where commands_ :: p -> Maybe [ChatBotCommand] instance HasCommands Preferences where commands_ Preferences {commands} = commands +instance HasField "_json" Preferences PrefsJSON where + hasField p@Preferences {_json} = (\j -> p {_json = j}, _json) + data GroupFeature = GFTimedMessages | GFDirectMessages @@ -371,12 +405,16 @@ data GroupPreferences = GroupPreferences sessions :: Maybe SessionsGroupPreference, comments :: Maybe CommentsGroupPreference, signMessages :: Maybe SignMessagesGroupPreference, - commands :: Maybe [ChatBotCommand] + commands :: Maybe [ChatBotCommand], + _json :: PrefsJSON } deriving (Eq, Show) instance HasCommands GroupPreferences where commands_ GroupPreferences {commands} = commands +instance HasField "_json" GroupPreferences PrefsJSON where + hasField p@GroupPreferences {_json} = (\j -> p {_json = j}, _json) + data ChatBotCommand = CBCCommand { keyword :: Text, -- "order" @@ -506,7 +544,8 @@ toChatPrefs FullPreferences {timedMessages, fullDelete, reactions, voice, files, files = Just files, calls = Just calls, sessions = Just sessions, - commands = Just cmds + commands = Just cmds, + _json = PrefsJSON Nothing } defaultChatPrefs :: FullPreferences @@ -523,7 +562,7 @@ defaultChatPrefs = } emptyChatPrefs :: Preferences -emptyChatPrefs = Preferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +emptyChatPrefs = Preferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing (PrefsJSON Nothing) defaultGroupPrefs :: FullGroupPreferences defaultGroupPrefs = @@ -545,7 +584,7 @@ defaultGroupPrefs = } emptyGroupPrefs :: GroupPreferences -emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing (PrefsJSON Nothing) businessGroupPrefs :: Preferences -> GroupPreferences businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice, files, sessions, commands} = @@ -580,7 +619,8 @@ defaultBusinessGroupPrefs = sessions = Just $ SessionsGroupPreference FEOn Nothing, comments = Just $ CommentsGroupPreference FEOff Nothing, signMessages = Just $ SignMessagesGroupPreference FEOff, - commands = Nothing + commands = Nothing, + _json = PrefsJSON Nothing } data TimedMessagesPreference = TimedMessagesPreference @@ -1092,7 +1132,8 @@ toGroupPreferences groupPreferences@FullGroupPreferences {commands = ListDef cmd sessions = pref SGFSessions, comments = pref SGFComments, signMessages = pref SGFSignMessages, - commands = Just cmds + commands = Just cmds, + _json = PrefsJSON Nothing } where pref :: SGroupFeature f -> Maybe (GroupFeaturePreference f) @@ -1192,13 +1233,22 @@ instance FromJSON SessionsPreference where $(J.deriveJSON (taggedObjectJSON $ dropPrefix "CBC") ''ChatBotCommand) -$(J.deriveJSON defaultJSON ''Preferences) +$(J.deriveToJSON defaultJSON ''Preferences) + +chatPrefsP :: Value -> JT.Parser Preferences +chatPrefsP = $(J.mkParseJSON defaultJSON ''Preferences) + +instance FromJSON Preferences where + parseJSON v = keepPrefsJSON v <$> chatPrefsP v + +chatPrefsFromRow :: Maybe Text -> Maybe Text -> Maybe Preferences +chatPrefsFromRow = prefsFromRow_ chatPrefsP instance ToField Preferences where toField = toField . encodeJSON instance FromField Preferences where - fromField = fromTextField_ decodeJSON + fromField = fromTextField_ $ decodePrefs chatPrefsP $(J.deriveJSON defaultJSON ''GroupPreference) @@ -1240,13 +1290,22 @@ instance FromJSON CommentsGroupPreference where parseJSON v = $(J.mkParseJSON defaultJSON ''CommentsGroupPreference) v omittedField = Just CommentsGroupPreference {enable = FEOff, duration = Nothing} -$(J.deriveJSON defaultJSON ''GroupPreferences) +$(J.deriveToJSON defaultJSON ''GroupPreferences) + +groupPrefsP :: Value -> JT.Parser GroupPreferences +groupPrefsP = $(J.mkParseJSON defaultJSON ''GroupPreferences) + +instance FromJSON GroupPreferences where + parseJSON v = keepPrefsJSON v <$> groupPrefsP v + +groupPrefsFromRow :: Maybe Text -> Maybe Text -> Maybe GroupPreferences +groupPrefsFromRow = prefsFromRow_ groupPrefsP instance ToField GroupPreferences where toField = toField . encodeJSON instance FromField GroupPreferences where - fromField = fromTextField_ decodeJSON + fromField = fromTextField_ $ decodePrefs groupPrefsP $(J.deriveJSON defaultJSON ''FullPreferences) diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 633bbe876c..554398520e 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} @@ -12,6 +13,7 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.List (isInfixOf) import qualified Data.List.NonEmpty as L +import Data.Maybe (fromMaybe) import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Simplex.Chat.Library.Internal (decodeLinkUserData, encodeShortLinkData) import Simplex.Chat.Protocol @@ -33,6 +35,41 @@ protocolTests = do decodeChatMessageTest shortLinkDataTests batchLimitTests + preferencesJSONTests + +preferencesJSONTests :: Spec +preferencesJSONTests = describe "preferences JSON" $ do + it "stores no JSON when preferences encode to what was received" $ do + ps <- prefs "{\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"no\"}}" + storedJSON ps `shouldBe` Nothing + it "stores no JSON when group preferences encode to what was received" $ do + ps <- groupPrefs "{\"voice\":{\"enable\":\"on\"},\"reactions\":{\"enable\":\"off\"}}" + storedJSON ps `shouldBe` Nothing + it "stores JSON with a preference that is not defined" $ do + let s = "{\"voice\":{\"enable\":\"on\"},\"polls\":{\"enable\":\"on\"}}" + ps <- groupPrefs s + storedJSON ps `shouldBe` Just (object s) + J.toJSON ps `shouldBe` object "{\"voice\":{\"enable\":\"on\"}}" + it "stores JSON with a field that is not defined in a preference" $ do + let s = "{\"voice\":{\"enable\":\"on\",\"exceptRole\":\"observer\"}}" + ps <- groupPrefs s + storedJSON ps `shouldBe` Just (object s) + it "reads the received preferences from the stored JSON" $ + groupPrefsFromRow (Just "{\"voice\":{\"enable\":\"on\"}}") (Just "{\"voice\":{\"enable\":\"off\"}}") + `shouldBe` groupPrefs_ "{\"voice\":{\"enable\":\"off\"}}" + it "reads the stored preferences when the received JSON does not parse" $ + groupPrefsFromRow (Just "{\"voice\":{\"enable\":\"on\"}}") (Just "{\"voice\":{\"enable\":\"sometimes\"}}") + `shouldBe` groupPrefs_ "{\"voice\":{\"enable\":\"on\"}}" + where + prefs :: ByteString -> IO Preferences + prefs = either fail pure . J.eitherDecodeStrict' + groupPrefs :: ByteString -> IO GroupPreferences + groupPrefs = either fail pure . J.eitherDecodeStrict' + groupPrefs_ :: ByteString -> Maybe GroupPreferences + groupPrefs_ = J.decodeStrict' + storedJSON ps = J.decodeStrictText =<< snd (prefsToRow $ Just ps) :: Maybe J.Value + object :: ByteString -> J.Value + object s = fromMaybe (error $ "not JSON: " <> B.unpack s) $ J.decodeStrict' s batchLimitTests :: Spec batchLimitTests = describe "Chat message batch limits" $ do @@ -133,10 +170,10 @@ s #==# msg = do s ==# msg testChatPreferences :: Maybe Preferences -testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, files = Nothing, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}, sessions = Nothing, commands = Nothing} +testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, files = Nothing, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}, sessions = Nothing, commands = Nothing, _json = PrefsJSON Nothing} testGroupPreferences :: Maybe GroupPreferences -testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, signMessages = Nothing, commands = Nothing} +testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, signMessages = Nothing, commands = Nothing, _json = PrefsJSON Nothing} testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, description = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} From 2214b8990ccd00ead8dcd5ddfffdd506b2f37f39 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 24 Sep 2026 07:16:35 +0100 Subject: [PATCH 6/9] website: update cf page (#7577) Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- website/src/crowdfunding.html | 259 ++++------------------------------ 1 file changed, 24 insertions(+), 235 deletions(-) diff --git a/website/src/crowdfunding.html b/website/src/crowdfunding.html index 32fdac1ba5..a4faa67dc8 100644 --- a/website/src/crowdfunding.html +++ b/website/src/crowdfunding.html @@ -220,61 +220,6 @@ templateEngineOverride: njk color: #dfeaff; } - .cf-hero-offer { - position: relative; - margin-top: calc(var(--sec-vhu) * 6.3); - width: calc(var(--sec-vwu) * 56); - } - .cf-hero-offer p { font-family: "Manrope", sans-serif; } - .cf-hero-offer .cf-offer-lead { - font-weight: 600; - font-size: calc(var(--sec-vwu) * 2.1); - line-height: 1.4; - color: #ffffff; - font-variant-numeric: tabular-nums; - } - .cf-hero-offer .cf-offer-lead b { - font-weight: 700; - background: linear-gradient(90deg, #019bfe 0%, #64fdff 58%, #c8feff 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - color: transparent; - } - .cf-hero-offer .cf-offer-lead span { white-space: nowrap; } - .cf-hero-offer .cf-offer-terms { - font-weight: 300; - font-size: calc(var(--sec-vwu) * 1.55); - line-height: 1.55; - color: #dfeaff; - margin-top: calc(var(--sec-vhu) * 0.4); - } - .cf-hero-offer .cf-offer-terms a { position: relative; color: inherit; text-decoration: none; } - .cf-hero-offer .cf-offer-terms abbr { text-decoration: underline dotted; text-underline-offset: 3px; cursor: pointer; } - .cf-hero-offer .cf-offer-terms a::after { - content: attr(data-tip); - position: absolute; - left: calc(100% + 12px); - top: 50%; - transform: translateY(-50%); - white-space: nowrap; - padding: 7px 12px; - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, .18); - background: rgba(5, 9, 32, .96); - color: #dfeaff; - font-family: "Manrope", sans-serif; - font-weight: 400; - font-size: calc(var(--sec-vwu) * 1.1); - line-height: 1.4; - opacity: 0; - visibility: hidden; - transition: opacity .12s ease; - pointer-events: none; - } - .cf-hero-offer .cf-offer-terms a:hover::after, - .cf-hero-offer .cf-offer-terms a:focus-visible::after { opacity: 1; visibility: visible; } - /* ---- close: same left-aligned layout as the hero ---- */ .cf-close .area { position: relative; display: flex; align-items: flex-end; padding: 0 calc(var(--sec-vwu) * 7.5) calc(var(--sec-vhu) * 22) 0; } @@ -655,7 +600,7 @@ templateEngineOverride: njk --cf-phone-h: calc(var(--cf-phone-w) / 0.4914); --cf-phone-part: 0.55; /* bottom of the hero text block, measured from the section top */ - --cf-hero-text-b: calc(var(--cf-top-base) + max(0px, calc(-1 * var(--cf-area-off))) + 92.5vw); + --cf-hero-text-b: calc(var(--cf-top-base) + max(0px, calc(-1 * var(--cf-area-off))) + 75vw); /* the arc crest starts 0.2678 down the cover image, which is 2.349 phone widths tall and sits 0.4034 phone heights above the phone top: the phone may rise no higher than this */ @@ -714,19 +659,6 @@ templateEngineOverride: njk } .cf-close-offer p { font-size: calc(var(--sec-vwu) * 3.6); } .cf-stats { top: 0; gap: calc(var(--sec-vwu) * 8); margin-top: calc(var(--sec-vhu) * 3); } - .cf-hero .cf-hero-offer { - position: relative; - top: 0; - width: auto; - z-index: 4; - margin-top: calc(var(--sec-vhu) * 2); - padding: 10px var(--cf-text-pad) 12px; - margin-left: calc(-1 * var(--cf-gutter)); - margin-right: calc(-1 * var(--cf-gutter)); - background: linear-gradient(to bottom, rgba(2, 7, 29, 0) 0%, rgba(2, 7, 29, var(--cf-offer-veil, 0)) 22%, rgba(2, 7, 29, var(--cf-offer-veil, 0)) 78%, rgba(2, 7, 29, 0) 100%); - } - .cf-hero-offer .cf-offer-lead { font-size: calc(var(--sec-vwu) * 4.2); line-height: 1.3; white-space: nowrap; } - .cf-hero-offer .cf-offer-terms { font-size: calc(var(--sec-vwu) * 4); margin-top: calc(var(--sec-vhu) * 0.8); } .cf-stat img { width: calc(var(--sec-vwu) * 8); } .cf-stat b { font-size: calc(var(--sec-vwu) * 6.5); } .cf-stat span { font-size: calc(var(--sec-vwu) * 3.4); } @@ -826,11 +758,9 @@ templateEngineOverride: njk .cf-tag .n { font-size: 3.54cqh; line-height: 1.3; } } - /* tall phones (modern ratios, with or without the browser bars): the headline takes the - room the screen has; short ones keep the smaller size. */ @media screen and (max-width: 959px) and (orientation: portrait) and (max-aspect-ratio: 62/100) { .cf-hero h1 { font-size: calc(var(--sec-vwu) * 9.6); } - :root { --cf-hero-text-b: calc(var(--cf-top-base) + max(0px, calc(-1 * var(--cf-area-off))) + 91vw); } + :root { --cf-hero-text-b: calc(var(--cf-top-base) + max(0px, calc(-1 * var(--cf-area-off))) + 72.8vw); } } /* 320pt phones: keep the headline at three lines */ @@ -1106,10 +1036,6 @@ templateEngineOverride: njk
520KMonthly users
-
-

SimpleX name for 7 years

-

Invest $500+ by the end of September 22, AoE.

-
@@ -1231,13 +1157,13 @@ templateEngineOverride: njk
-

Invest $500+ by November 22 during early bird terms and get a SimpleX public name for your channel or business for 5 years, ahead of public launch.

+

Invest $500+ during early bird terms and get a SimpleX public name for your channel or business for 5 years, ahead of public launch.

-