mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 22:18:36 +00:00
ui: show countdown until SimpleX domains sale launch (#7434)
* ui: show countdown until SimpleX domains sale launch * list * ios: update * update kotlin, ios countdown * kotlin countdown * fix formats * padding * update --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny @ SimpleX Chat
parent
3da3340287
commit
a2de9f13df
@@ -802,6 +802,8 @@ private func saveAddressSettings(_ settings: AddressSettingsState, _ savedSettin
|
||||
}
|
||||
}
|
||||
|
||||
private let simplexNameSaleStart = Calendar(identifier: .gregorian).date(from: DateComponents(timeZone: TimeZone(identifier: "UTC"), year: 2026, month: 12, day: 12, hour: 18))!
|
||||
|
||||
struct SetSimplexDomainView: View {
|
||||
let title: LocalizedStringKey
|
||||
let footer: LocalizedStringKey
|
||||
@@ -815,6 +817,8 @@ struct SetSimplexDomainView: View {
|
||||
@State private var original = ""
|
||||
@State private var didSave = false
|
||||
@State private var editing = false
|
||||
@State private var timeToSaleStart = simplexNameSaleStart.timeIntervalSinceNow
|
||||
@State private var saleTimer: Timer? = nil
|
||||
@FocusState private var nameFocused: Bool
|
||||
|
||||
init(title: LocalizedStringKey, footer: LocalizedStringKey, prompt: String, simplexName: String, broadcastWarning: String? = nil, save: @escaping (String?) async -> Bool) {
|
||||
@@ -873,7 +877,7 @@ struct SetSimplexDomainView: View {
|
||||
Section {
|
||||
if editing {
|
||||
Button {
|
||||
openBrowserAlert(uri: "https://github.com/simplex-chat/simplex-chat/blob/master/docs/guide/register-simplex-name.md")
|
||||
openBrowserAlert(uri: "https://simplex.domains/#testing")
|
||||
} label: {
|
||||
HStack {
|
||||
Text("How to register a test name")
|
||||
@@ -901,15 +905,39 @@ struct SetSimplexDomainView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(verbatim: saleCountdown(timeToSaleStart))
|
||||
Text(timeToSaleStart > 0 ? "until you can register a SimpleX domain" : "Update the app to register a SimpleX domain")
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("SimpleX name sale starts in")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("Crowdfunding investors can reserve their name before sale starts: [simplex.domains](https://simplex.domains/)")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.onAppear {
|
||||
if editing {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { nameFocused = true }
|
||||
}
|
||||
if timeToSaleStart > 0 {
|
||||
saleTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { t in
|
||||
timeToSaleStart = simplexNameSaleStart.timeIntervalSinceNow
|
||||
if timeToSaleStart <= 0 { t.invalidate() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
saleTimer?.invalidate()
|
||||
saleTimer = nil
|
||||
if !didSave, !saving, changed, isValid {
|
||||
let domain = normalized(simplexName)
|
||||
let saveName = save
|
||||
@@ -948,6 +976,21 @@ struct SetSimplexDomainView: View {
|
||||
: addSimplexTLD((t.hasPrefix("@") || t.hasPrefix("#") ? String(t.dropFirst()) : t).lowercased())
|
||||
}
|
||||
|
||||
private func saleCountdown(_ remaining: TimeInterval) -> String {
|
||||
let total = max(0, Int(remaining))
|
||||
let days = total / 86400
|
||||
let dayStr = String.localizedStringWithFormat(
|
||||
days == 1
|
||||
? NSLocalizedString("%d day", comment: "time interval")
|
||||
: NSLocalizedString("%d days", comment: "time interval"),
|
||||
days
|
||||
)
|
||||
return dayStr + " " + String.localizedStringWithFormat(
|
||||
NSLocalizedString("%02d hrs %02d min %02d sec", comment: "countdown"),
|
||||
total / 3600 % 24, total / 60 % 60, total % 60
|
||||
)
|
||||
}
|
||||
|
||||
private func addSimplexTLD(_ d: String) -> String {
|
||||
if d.contains(".") { d } else { "\(d).simplex" }
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ afterEvaluate {
|
||||
val fontLtGtRegex = Regex("[^>]*>.*<font[^>]*>.*</font>.*</string>")
|
||||
val unbracketedColorRegex = Regex("color=#[abcdefABCDEF0-9]{3,6}")
|
||||
val correctHtmlRegex = Regex("[^>]*>.*<b>.*</b>.*</string>|[^>]*>.*<i>.*</i>.*</string>|[^>]*>.*<u>.*</u>.*</string>|[^>]*>.*<font[^>]*>.*</font>.*</string>")
|
||||
val possibleFormat = listOf("s", "d", "1\$s", "2\$s", "3\$s", "4\$s", "1\$d", "2\$d", "3\$d", "4\$d", "2s", "f")
|
||||
val possibleFormat = listOf("s", "d", "1\$s", "2\$s", "3\$s", "4\$s", "1\$d", "2\$d", "3\$d", "4\$d", "1\$02d", "2\$02d", "3\$02d", "2s", "f")
|
||||
|
||||
fun String.id(): String = replace("<string name=\"", "").trim().substringBefore("\"")
|
||||
|
||||
@@ -239,7 +239,7 @@ afterEvaluate {
|
||||
if (was.length == substring.length) break
|
||||
}
|
||||
return if (formats.any { it.startsWith("1$") || it.startsWith("2$") || it.startsWith("3$") || it.startsWith("4$") }) {
|
||||
formats.sortedBy { it.trim('s', 'd', 'f', '$').toIntOrNull() ?: throw Exception("Formatting don't have positional arguments: $this \nin $filepath") }
|
||||
formats.sortedBy { it.substringBefore('$').toIntOrNull() ?: throw Exception("Formatting don't have positional arguments: $this \nin $filepath") }
|
||||
} else {
|
||||
formats
|
||||
}
|
||||
|
||||
+43
-2
@@ -14,7 +14,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.*
|
||||
@@ -24,10 +25,14 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.*
|
||||
|
||||
// Each dot-separated label is ASCII letters/digits with single internal hyphens (mirrors simplexmq SimplexName.hs nameLabelP).
|
||||
private val simplexNameLabelRegex = Regex("[A-Za-z0-9]+(-[A-Za-z0-9]+)*")
|
||||
|
||||
private val simplexNameSaleStart = LocalDateTime(2026, 12, 12, 18, 0).toInstant(TimeZone.UTC)
|
||||
private const val SIMPLEX_DOMAINS_URL = "https://simplex.domains/"
|
||||
|
||||
// Set the user's own (prefix "@") or a channel's (prefix "#") SimpleX name.
|
||||
// The field is prefilled with the full prefixed name; `save` receives the encoded name (or null to
|
||||
// clear) and returns true on success (it shows its own error alert otherwise).
|
||||
@@ -128,6 +133,13 @@ fun SetSimplexDomainView(
|
||||
}
|
||||
}
|
||||
|
||||
fun saleCountdown(msRemaining: Long): String {
|
||||
val total = (msRemaining / 1000).coerceAtLeast(0)
|
||||
val days = total / 86400
|
||||
val dayStr = String.format(generalGetString(if (days == 1L) MR.strings.ttl_day else MR.strings.ttl_days), days)
|
||||
return dayStr + " " + String.format(generalGetString(MR.strings.countdown_hrs_min_sec), total / 3600 % 24, total / 60 % 60, total % 60)
|
||||
}
|
||||
|
||||
ModalView(close = { onClose(close) }, cardScreen = true) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(title)
|
||||
@@ -163,7 +175,7 @@ fun SetSimplexDomainView(
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_open_in_new),
|
||||
stringResource(MR.strings.register_test_name),
|
||||
{ openBrowserAlert("https://github.com/simplex-chat/simplex-chat/blob/master/docs/guide/register-simplex-name.md", uriHandler) },
|
||||
{ openBrowserAlert("https://simplex.domains/#testing", uriHandler) },
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary
|
||||
)
|
||||
@@ -179,6 +191,35 @@ fun SetSimplexDomainView(
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
val msToSaleStart = remember { mutableStateOf(simplexNameSaleStart.toEpochMilliseconds() - System.currentTimeMillis()) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (msToSaleStart.value > 0) {
|
||||
delay(1000)
|
||||
msToSaleStart.value = simplexNameSaleStart.toEpochMilliseconds() - System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
SectionView(stringResource(MR.strings.simplex_name_sales)) {
|
||||
SectionItemView {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(saleCountdown(msToSaleStart.value))
|
||||
Text(
|
||||
stringResource(if (msToSaleStart.value > 0) MR.strings.until_register_simplex_domain else MR.strings.update_app_register_simplex_domain),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.simplex_name_sales_footer))
|
||||
append(" ")
|
||||
withLink(LinkAnnotation.Url(SIMPLEX_DOMAINS_URL) { uriHandler.openUriCatching(SIMPLEX_DOMAINS_URL) }) {
|
||||
withStyle(SpanStyle(color = MaterialTheme.colors.primary)) {
|
||||
append("simplex.domains")
|
||||
}
|
||||
}
|
||||
})
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +397,11 @@
|
||||
<string name="get_simplex_name_beta">Get SimpleX name (BETA)</string>
|
||||
<string name="channel_simplex_name">Channel SimpleX name</string>
|
||||
<string name="register_test_name">How to register a test name</string>
|
||||
<string name="simplex_name_sales">SimpleX name sale starts in</string>
|
||||
<string name="until_register_simplex_domain">until you can register a SimpleX domain</string>
|
||||
<string name="update_app_register_simplex_domain">Update the app to register a SimpleX domain</string>
|
||||
<string name="countdown_hrs_min_sec">%1$02d hrs %2$02d min %3$02d sec</string>
|
||||
<string name="simplex_name_sales_footer">Crowdfunding investors can reserve their name before sale starts:</string>
|
||||
<string name="remove_name">Remove name</string>
|
||||
<string name="save_verb">Save</string>
|
||||
<string name="edit_verb">Edit</string>
|
||||
|
||||
Reference in New Issue
Block a user