Merge branch 'stable'
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -58,3 +58,54 @@ struct ZoomableScrollView<Content: View>: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ZoomablePageView<Content: View>: 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<Content>
|
||||
|
||||
init(hostingController: UIHostingController<Content>) {
|
||||
self.hostingController = hostingController
|
||||
}
|
||||
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
hostingController.view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<VersionDescription> = 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<CrowdfundingSlide> = 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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2736,9 +2736,10 @@
|
||||
<string name="v6_5_safe_web_links_descr">- opt-in to send link previews.\n- use SOCKS proxy if enabled.\n- prevent hyperlink phishing.\n- remove link tracking.</string>
|
||||
<string name="v6_5_non_profit_governance">Non-profit governance</string>
|
||||
<string name="v6_5_non_profit_governance_descr">To make SimpleX Network last.</string>
|
||||
<!-- <string name="v7_0_invest">Invest in SimpleX Chat</string> -->
|
||||
<!-- <string name="v7_0_invest_descr">Equity crowdfunding launched!</string> -->
|
||||
<!-- <string name="v7_0_invest_learn_more">Learn more on Wefunder</string> -->
|
||||
<string name="v7_0_invest" translatable="false">You can now invest in SimpleX Chat! 🚀</string>
|
||||
<string name="v7_0_invest_descr" translatable="false">Crowdfunding on Wefunder.</string>
|
||||
<string name="v7_0_crowdfunding" translatable="false">Crowdfunding on Wefunder</string>
|
||||
<string name="v7_0_invest_learn_more" translatable="false">Learn more on Wefunder</string>
|
||||
<string name="v7_0_simplex_names">SimpleX public names (BETA)</string>
|
||||
<string name="v7_0_simplex_names_descr">Public names for your channel or business.</string>
|
||||
<string name="v7_0_channels">Better channels 📢</string>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 180 KiB |