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:
Evgeny
2026-09-13 17:42:57 +01:00
committed by Evgeny Poberezkin
co-authored by Evgeny @ SimpleX Chat
parent 3da3340287
commit a2de9f13df
4 changed files with 94 additions and 5 deletions
@@ -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" }
}
+2 -2
View File
@@ -212,7 +212,7 @@ afterEvaluate {
val fontLtGtRegex = Regex("[^>]*>.*&lt;font[^>]*&gt;.*&lt;/font&gt;.*</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
}
@@ -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>