diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 6ea24ec91c..937a0f089c 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -42,7 +42,7 @@ private struct FeatureView { let view: () -> any View } -private let isInUS = SKStorefront().countryCode == "USA" +let isInUS = SKStorefront().countryCode == "USA" private let versionDescriptions: [VersionDescription] = [ VersionDescription( @@ -668,12 +668,12 @@ private let versionDescriptions: [VersionDescription] = [ ] ), VersionDescription( - version: "v7.0", + version: "v7.0 ", post: nil, features: (isInUS ? [ .view(FeatureView( icon: nil, - title: "Invest in SimpleX Chat", + title: "You can now invest in SimpleX Chat", view: { InvestInSimpleXChat() } )) ] : []) + [ @@ -772,38 +772,130 @@ fileprivate struct CreateUpdateAddressShortLink: View { } fileprivate struct InvestInSimpleXChat: View { - @Environment(\.colorScheme) var colorScheme @EnvironmentObject var theme: AppTheme + @State private var showGetStakeSheet = false var body: some View { - HStack(alignment: .top, spacing: 8) { - VStack(alignment: .leading, spacing: 4) { - HStack(alignment: .center, spacing: 4) { - Image(systemName: "dollarsign.circle") - .symbolRenderingMode(.monochrome) - .foregroundColor(theme.colors.secondary) - .frame(minWidth: 30, alignment: .center) - Text(verbatim: "Invest in SimpleX Chat").font(.title3).bold() - } - Text(verbatim: "Equity crowdfunding launched!") - .multilineTextAlignment(.leading) - .lineLimit(2) - if let url = URL("https://wefunder.com/simplexchat") { - ExternalLink(destination: url) { - HStack { - Text(verbatim: "Learn more on Wefunder") - Image(systemName: "arrow.up.right.circle") - } - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - Image(colorScheme == .light ? "own-stake" : "own-stake-light") + VStack(alignment: .leading, spacing: 4) { + Text("You can now invest in SimpleX Chat! 🚀").font(.title3).bold() + (Text("Crowdfunding on Wefunder.") + Text(verbatim: " ") + Text("Learn more").foregroundColor(theme.colors.primary)) + .multilineTextAlignment(.leading) + .onTapGesture { showGetStakeSheet = true } + #if SIMPLEX_ASSETS + Image("crowdfunding_1") .resizable() - .scaledToFill() - .frame(width: UIScreen.main.bounds.width / 5) + .scaledToFit() + .cornerRadius(12) + .padding(.vertical, 4) + .onTapGesture { showGetStakeSheet = true } + #endif } .frame(maxWidth: .infinity, alignment: .leading) + .sheet(isPresented: $showGetStakeSheet) { + GetStakeView(fromSettings: false) + } + } +} + +fileprivate let getStakeSlides: [(image: String, heading: String, info: String?, text: String)] = [ + ( + "crowdfunding_1", + "The first and the only messaging network without any user IDs", + nil, + "By investing, you can benefit from the company growth, and help us build the future of private and secure communications." + ), + ( + "crowdfunding_2", + "480,000+ users joined on their own", + nil, + "SimpleX users have been more than doubling every year without any paid marketing, and donated over $650,000." + ), + ( + "crowdfunding_3", + "Developers already bet on SimpleX success", + "Independent developers created moderation and AI bots, Telegram bridges, and a public server registry.", + "Every service developers build on SimpleX Network may increase its value, and bring new users to SimpleX Chat." + ), + ( + "crowdfunding_4", + "Revenue plan: free for users, channels & businesses pay", + "SimpleX Chat plans to earn from the infrastructure and services that creators, businesses and large communities need as they grow.", + "Read about how we plan to make SimpleX Chat and network profitable, and about all the investment terms on Wefunder." + ), +] + +private let wefunderURL = URL(string: "https://wefunder.com/simplex.chat?utm_source=app")! + +private let simplexCrowdfundingURL = URL(string: "simplex:/a#JxGcOA1_QhlmVFzYYabloMbvMZk5Y9d9iS3ITDnhzYo?h=smp11.simplex.im")! + +struct GetStakeView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var chatModel: ChatModel + var fromSettings: Bool + + var body: some View { + ZoomablePageView { + VStack(alignment: .leading, spacing: 18) { + Text(verbatim: "Get a stake in\nSimpleX Chat") + .font(.largeTitle) + .bold() + .fixedSize(horizontal: false, vertical: true) + .if(!fromSettings) { $0.padding(.top) } + if fromSettings { + slideImage(getStakeSlides[0]) + } + (Text(verbatim: getStakeSlides[0].text) + Text(verbatim: " Learn more and invest on Wefunder.").bold().foregroundColor(.accentColor)) + .multilineTextAlignment(.leading) + .onTapGesture { + UIApplication.shared.open(wefunderURL) + } + .padding(.bottom) + ForEach(getStakeSlides[1...3], id: \.image) { slide in + VStack(alignment: .leading) { + slideImage(slide) + Text(slide.text) + } + .padding(.bottom) + } + + Button { + UIApplication.shared.open(wefunderURL) + } label: { + Text(verbatim: "Learn more on Wefunder") + } + .buttonStyle(OnboardingButtonStyle()) + + Button { + dismiss() + DispatchQueue.main.async { + ChatModel.shared.appOpenUrl = simplexCrowdfundingURL + } + } label: { + Text(verbatim: "or ask SimpleX team") + .font(.callout) + } + .disabled(chatModel.chatRunning != true) + .frame(maxWidth: .infinity) + } + .padding() + } + .ignoresSafeArea(edges: .bottom) + .modifier(ThemedBackground(grouped: true)) + } + + @ViewBuilder + func slideImage(_ slide: (image: String, heading: String, info: String?, text: String?)) -> some View { + #if SIMPLEX_ASSETS + Image(slide.image) + .resizable() + .scaledToFit() + .cornerRadius(12) + #else + Text(slide.heading).font(.title3).bold() + if let info = slide.info { + Text(info) + } + #endif } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 4bd5f7db1b..ae317cd864 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -380,6 +380,17 @@ struct SettingsView: View { Text(verbatim: "v\(appVersion ?? "?")") } } + + if isInUS { + Section(header: Text("You can now invest in SimpleX Chat").foregroundColor(theme.colors.secondary)) { + NavigationLink { + GetStakeView(fromSettings: true) + .navigationBarTitle("", displayMode: .inline) + } label: { + settingsRow("dollarsign.circle", color: theme.colors.secondary) { Text("Crowdfunding on Wefunder") } + } + } + } } .navigationTitle("Your settings") .modifier(ThemedBackground(grouped: true)) diff --git a/apps/ios/Shared/Views/ZoomableScrollView.swift b/apps/ios/Shared/Views/ZoomableScrollView.swift index 83528b593a..87eb645822 100644 --- a/apps/ios/Shared/Views/ZoomableScrollView.swift +++ b/apps/ios/Shared/Views/ZoomableScrollView.swift @@ -58,3 +58,54 @@ struct ZoomableScrollView: UIViewRepresentable { } } } + +struct ZoomablePageView: UIViewRepresentable { + private var content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + func makeUIView(context: Context) -> UIScrollView { + let scrollView = UIScrollView() + scrollView.delegate = context.coordinator + scrollView.maximumZoomScale = 5 + scrollView.minimumZoomScale = 1 + scrollView.bouncesZoom = true + scrollView.backgroundColor = .clear + + let hostedView = context.coordinator.hostingController.view! + hostedView.backgroundColor = .clear + hostedView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(hostedView) + NSLayoutConstraint.activate([ + hostedView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + hostedView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + hostedView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + hostedView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + hostedView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) + ]) + + return scrollView + } + + func makeCoordinator() -> Coordinator { + Coordinator(hostingController: UIHostingController(rootView: self.content)) + } + + func updateUIView(_ uiView: UIScrollView, context: Context) { + context.coordinator.hostingController.rootView = self.content + } + + class Coordinator: NSObject, UIScrollViewDelegate { + var hostingController: UIHostingController + + init(hostingController: UIHostingController) { + self.hostingController = hostingController + } + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + hostingController.view + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt index ee63846657..cf2ceaf2d6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt @@ -31,7 +31,8 @@ fun AppBarTitle( val connection = if (enableAlphaChanges) handler?.connection else null LaunchedEffect(title) { if (enableAlphaChanges) { - handler?.title?.value = title + // the app bar shows a single line, so the line breaks of the large title are replaced with spaces + handler?.title?.value = title.replace("\n", " ") } else { handler?.connection?.scrollTrackingEnabled = false } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index ea95bc2045..beadf49214 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -1,6 +1,10 @@ package chat.simplex.common.views.onboarding import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.calculatePan +import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -8,15 +12,29 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.BuildConfigCommon @@ -35,6 +53,7 @@ import chat.simplex.common.views.usersettings.showAddShortLinkAlert import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource +import kotlin.math.absoluteValue @Composable fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Boolean = false, close: () -> Unit) { @@ -914,14 +933,15 @@ private val versionDescriptions: List = listOf( ) ), VersionDescription( - version = "v7.0", + // the trailing space differs from the previously released "v7.0", so that What's new is shown again + version = "v7.0 ", post = null, features = listOf( -// VersionFeature.FeatureView( -// icon = null, -// titleId = MR.strings.v7_0_invest, -// view = { _ -> InvestInSimpleXChatView() } -// ), + VersionFeature.FeatureView( + icon = null, + titleId = MR.strings.v7_0_invest, + view = { modalManager -> InvestInSimpleXChatView(modalManager) } + ), VersionFeature.FeatureDescription( icon = MR.images.ic_alternate_email, titleId = MR.strings.v7_0_simplex_names, @@ -956,57 +976,253 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean { return v != lastVersion } -// private const val WEFUNDER_URL = "https://wefunder.com/simplexchat" -// -// @Composable -// private fun InvestInSimpleXChatView() { -// if (platform.androidIsPlayStoreBuild) { -// LaunchedEffect(Unit) { if (androidPlayStoreCountry.value == null) platform.androidLoadPlayStoreCountry() } -// if (androidPlayStoreCountry.value != "US") return -// } -// val uriHandler = LocalUriHandler.current -// Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(bottom = 12.dp)) { -// Column(modifier = Modifier.weight(1f)) { -// Row( -// verticalAlignment = Alignment.CenterVertically, -// horizontalArrangement = Arrangement.spacedBy(8.dp), -// modifier = Modifier.padding(bottom = 4.dp) -// ) { -// Icon(painterResource(MR.images.ic_redeem), stringResource(MR.strings.v7_0_invest), tint = MaterialTheme.colors.secondary) -// Text( -// generalGetString(MR.strings.v7_0_invest), -// maxLines = 2, -// overflow = TextOverflow.Ellipsis, -// style = MaterialTheme.typography.h4, -// fontWeight = FontWeight.Medium, -// modifier = Modifier.padding(bottom = 6.dp) -// ) -// } -// Text(generalGetString(MR.strings.v7_0_invest_descr), fontSize = 15.sp, modifier = Modifier.padding(bottom = 4.dp)) -// Row( -// verticalAlignment = Alignment.CenterVertically, -// horizontalArrangement = Arrangement.spacedBy(8.dp), -// modifier = Modifier -// .clickable( -// interactionSource = remember { MutableInteractionSource() }, -// indication = null -// ) { -// uriHandler.openExternalLink(WEFUNDER_URL) -// } -// ) { -// Text(stringResource(MR.strings.v7_0_invest_learn_more), color = MaterialTheme.colors.primary, fontSize = 15.sp) -// Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.v7_0_invest_learn_more), tint = MaterialTheme.colors.primary) -// } -// } -// if (BuildConfigCommon.SIMPLEX_ASSETS) { -// Image( -// painterResource(if (isInDarkTheme()) MR.images.own_stake_light else MR.images.own_stake), -// contentDescription = null, -// modifier = Modifier.width(80.dp) -// ) -// } -// } -// } +private const val WEFUNDER_URL = "https://wefunder.com/simplex.chat" + +private const val CROWDFUNDING_CONTACT_URI = "simplex:/a#JxGcOA1_QhlmVFzYYabloMbvMZk5Y9d9iS3ITDnhzYo?h=smp11.simplex.im" + +// the center modal takes the remaining width of the window, so the image is limited to its design width +private val MAX_CROWDFUNDING_IMAGE_WIDTH = DEFAULT_MIN_CENTER_MODAL_WIDTH + +// Google Play policy restricts promoting investments, so Play builds only show it in the US +@Composable +fun crowdfundingAvailable(): Boolean { + if (!platform.androidIsPlayStoreBuild) return true + LaunchedEffect(Unit) { + if (androidPlayStoreCountry.value == null) platform.androidLoadPlayStoreCountry() + } + return androidPlayStoreCountry.value == "US" +} + +@Composable +private fun InvestInSimpleXChatView(modalManager: ModalManager) { + if (!crowdfundingAvailable()) return + val showGetStake = { modalManager.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = false, close = close) } } + Column(modifier = Modifier.padding(bottom = 12.dp)) { + Text( + generalGetString(MR.strings.v7_0_invest), + style = MaterialTheme.typography.h4, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 6.dp) + ) + Text( + buildAnnotatedString { + append(generalGetString(MR.strings.v7_0_invest_descr)) + append(" ") + withStyle(SpanStyle(color = MaterialTheme.colors.primary)) { + append(generalGetString(MR.strings.learn_more)) + } + }, + fontSize = 15.sp, + modifier = Modifier + .pointerHoverIcon(PointerIcon.Hand) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = showGetStake + ) + ) + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(MR.images.crowdfunding_1), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .padding(top = 8.dp) + .widthIn(max = MAX_CROWDFUNDING_IMAGE_WIDTH) + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .pointerHoverIcon(PointerIcon.Hand) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = showGetStake + ) + ) + } + } +} + +private class CrowdfundingSlide( + val image: ImageResource, + val heading: String, + val info: String?, + val text: String, +) + +// not localized: the page is only shown to US investors, and the text duplicates the images +private val getStakeSlides: List = listOf( + CrowdfundingSlide( + MR.images.crowdfunding_1, + "The first and the only messaging network without any user IDs", + null, + "By investing, you can benefit from the company growth, and help us build the future of private and secure communications." + ), + CrowdfundingSlide( + MR.images.crowdfunding_2, + "480,000+ users joined on their own", + null, + "SimpleX users have been more than doubling every year without any paid marketing, and donated over \$650,000." + ), + CrowdfundingSlide( + MR.images.crowdfunding_3, + "Developers already bet on SimpleX success", + "Independent developers created moderation and AI bots, Telegram bridges, and a public server registry.", + "Every service developers build on SimpleX Network may increase its value, and bring new users to SimpleX Chat." + ), + CrowdfundingSlide( + MR.images.crowdfunding_4, + "Revenue plan: free for users, channels & businesses pay", + "SimpleX Chat plans to earn from the infrastructure and services that creators, businesses and large communities need as they grow.", + "Read about how we plan to make SimpleX Chat and network profitable, and about all the investment terms on Wefunder." + ), +) + +@Composable +fun GetStakeView(fromSettings: Boolean, close: () -> Unit) { + val uriHandler = LocalUriHandler.current + val stopped = chatModel.chatRunning.value == false + + @Composable + fun slideImage(slide: CrowdfundingSlide) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(slide.image), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(12.dp)).fullScreenOnClick(slide.image) + ) + } else { + Text(slide.heading, style = MaterialTheme.typography.h4, fontWeight = FontWeight.Medium) + if (slide.info != null) { + Text(slide.info, Modifier.padding(top = 4.dp), lineHeight = 24.sp) + } + } + } + + ColumnWithScrollBar(Modifier.pinchZoom().padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle("Get a stake in\nSimpleX Chat", withPadding = false) + // What's new already shows the image of the first slide, above the link that opens this page + if (fromSettings) { + slideImage(getStakeSlides[0]) + } + Text( + buildAnnotatedString { + append(getStakeSlides[0].text) + // only the link is clickable, the rest of the paragraph is not + withLink(LinkAnnotation.Url(WEFUNDER_URL) { uriHandler.openUriCatching(WEFUNDER_URL) }) { + withStyle(SpanStyle(color = MaterialTheme.colors.primary, fontWeight = FontWeight.Bold)) { + append(" Learn more and invest on Wefunder.") + } + } + }, + Modifier.padding(top = if (fromSettings) 8.dp else 0.dp), + lineHeight = 24.sp + ) + + getStakeSlides.drop(1).forEach { slide -> + Column(Modifier.padding(top = DEFAULT_PADDING * 1.5f)) { + slideImage(slide) + Text(slide.text, Modifier.padding(top = 8.dp), lineHeight = 24.sp) + } + } + + Column( + Modifier.fillMaxWidth().padding(top = DEFAULT_PADDING * 2), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OnboardingActionButton( + if (appPlatform.isAndroid) Modifier.fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.v7_0_invest_learn_more, + onboarding = null, + onclick = { uriHandler.openUriCatching(WEFUNDER_URL) } + ) + if (!chatModel.desktopNoUserNoRemote) { + TextButtonBelowOnboardingButton( + "or ask SimpleX team", + onClick = if (stopped) null else ({ + close() + uriHandler.openVerifiedSimplexUri(CROWDFUNDING_CONTACT_URI) + }) + ) + } + } + } +} + +// there is no pinch gesture with a mouse, so on desktop a slide is opened full screen instead +@Composable +private fun Modifier.fullScreenOnClick(image: ImageResource): Modifier { + if (!appPlatform.isDesktop) return this + return pointerHoverIcon(PointerIcon.Hand).clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + ModalManager.fullscreen.showCustomModal { close -> + BackHandler(onBack = close) + Box( + Modifier + .fillMaxSize() + .background(Color.Black) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = close), + contentAlignment = Alignment.Center + ) { + Image(painterResource(image), contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize()) + } + } + } +} + +private const val MAX_PAGE_ZOOM = 5f + +/** + * The slide images contain small text that is unreadable at screen width, so the page can be pinch-zoomed. + * Android only: pinch is unavailable with a mouse. + */ +@Composable +private fun Modifier.pinchZoom(): Modifier { + if (!appPlatform.isAndroid) return this + var scale by remember { mutableStateOf(1f) } + var offsetX by remember { mutableStateOf(0f) } + var offsetY by remember { mutableStateOf(0f) } + var size by remember { mutableStateOf(IntSize.Zero) } + return this + .onGloballyPositioned { size = it.size } + .graphicsLayer { + scaleX = scale + scaleY = scale + translationX = offsetX + translationY = offsetY + } + .pointerInput(Unit) { + awaitEachGesture { + // the initial pass, as the scroll of the same column is applied after this modifier and would take the gesture first + awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) + var taken: Boolean? = null + do { + val event = awaitPointerEvent(PointerEventPass.Initial) + val multiTouch = event.changes.count { it.pressed } > 1 + if (multiTouch || scale > 1f) { + scale = (scale * event.calculateZoom()).coerceIn(1f, MAX_PAGE_ZOOM) + val pan = event.calculatePan() + // the page is scaled around its center, so it can be panned by half of the overflow in each direction + val maxX = size.width * (scale - 1f) / 2 + val maxY = size.height * (scale - 1f) / 2 + val pannedY = offsetY + pan.y * scale + // the clamp is applied even when the gesture is not taken: at scale 1 both bounds + // are 0, which resets the offsets after zooming back out + offsetX = (offsetX + pan.x * scale).coerceIn(-maxX, maxX) + offsetY = pannedY.coerceIn(-maxY, maxY) + // two fingers always mean zoom, taken without a touch slop: waiting for one would let + // the scroll reach its own slop first and scroll the page. A one finger drag is left + // to the scroll at the edges, decided once so it cannot alternate mid drag + if (multiTouch) taken = true + else if (taken == null && pan.y != 0f) taken = pannedY.absoluteValue < maxY + if (taken == true) event.changes.forEach { if (it.pressed) it.consume() } + } + } while (event.changes.any { it.pressed }) + } + } +} @Composable fun CreateUpdateAddressShortLinkView(modalManager: ModalManager) { 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 96f36da6d7..8c134cb361 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 @@ -29,8 +29,10 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.helpers.* import chat.simplex.common.views.migration.MigrateFromDeviceView +import chat.simplex.common.views.onboarding.GetStakeView import chat.simplex.common.views.onboarding.SimpleXInfo import chat.simplex.common.views.onboarding.WhatsNewView +import chat.simplex.common.views.onboarding.crowdfundingAvailable import chat.simplex.common.views.usersettings.networkAndServers.NetworkAndServersView import chat.simplex.res.MR @@ -110,6 +112,17 @@ fun SettingsLayout( AppShutdownItem() AppVersionItem(showVersion) } + + if (crowdfundingAvailable()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.v7_0_invest)) { + SettingsActionItem( + painterResource(MR.images.ic_redeem), + stringResource(MR.strings.v7_0_crowdfunding), + { ModalManager.start.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = true, close = close) } } + ) + } + } SectionBottomSpacer() } } 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 ead51b31ea..739467b3ff 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -2736,9 +2736,10 @@ - opt-in to send link previews.\n- use SOCKS proxy if enabled.\n- prevent hyperlink phishing.\n- remove link tracking. Non-profit governance To make SimpleX Network last. - - - + You can now invest in SimpleX Chat! 🚀 + Crowdfunding on Wefunder. + Crowdfunding on Wefunder + Learn more on Wefunder SimpleX public names (BETA) Public names for your channel or business. Better channels 📢 diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_1.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_1.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_2.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_2.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_3.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_3.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_4.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_4.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_4.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_1@2x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_1@2x.jpg new file mode 100644 index 0000000000..715a6daf99 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_1@2x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_1@3x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_1@3x.jpg new file mode 100644 index 0000000000..f70ecb91db Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_1@3x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_2@2x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_2@2x.jpg new file mode 100644 index 0000000000..48bfe6caa7 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_2@2x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_2@3x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_2@3x.jpg new file mode 100644 index 0000000000..2bab828717 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_2@3x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_3@2x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_3@2x.jpg new file mode 100644 index 0000000000..704cae8710 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_3@2x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_3@3x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_3@3x.jpg new file mode 100644 index 0000000000..d7c1f4e088 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_3@3x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_4@2x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_4@2x.jpg new file mode 100644 index 0000000000..e39e0836cb Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_4@2x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_4@3x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_4@3x.jpg new file mode 100644 index 0000000000..ce9b4a86ef Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_4@3x.jpg differ