align app divergences

This commit is contained in:
Alain Brenzikofer
2026-09-30 10:55:55 +02:00
parent ddc18b326b
commit 86556408ba
6 changed files with 47 additions and 32 deletions
+15 -21
View File
@@ -1426,14 +1426,6 @@ private func showNameWarningAlert(
}
}
@MainActor private func nameChatId(_ chatInfo: ChatInfo?) -> ChatId? {
guard let chatInfo else { return nil }
if ChatModel.shared.getChat(chatInfo.id) == nil {
ChatModel.shared.addChat(Chat(chatInfo: chatInfo, chatItems: []))
}
return chatInfo.id
}
private func showOtherNameAlert(_ otherSimplexName: SimplexNameInfo, connectOtherButton: String, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)?) {
showAlert(
String.localizedStringWithFormat(
@@ -1610,7 +1602,6 @@ func planAndConnect(
case let .ok(contactSLinkData_, ownerVerification, addressChanged, existingChat_):
if let contactSLinkData = contactSLinkData_ {
logger.debug("planAndConnect, .contactAddress, .ok, short link data present")
let existingChatId = await nameChatId(filterKnownContact == nil ? existingChat_ : nil)
await MainActor.run {
showPrepareContactAlert(
connectionLink: connectionLink,
@@ -1620,7 +1611,7 @@ func planAndConnect(
connectOtherButton: connectOtherButton,
connectOtherLink: connectOtherLink,
addressChanged: addressChanged,
openExistingChat: existingChatId.map { chatId in { openKnownChat(chatId, dismiss: dismiss, cleanup: cleanup) } },
openExistingChat: (filterKnownContact == nil ? existingChat_ : nil).map { chatInfo in { openKnownChat(chatInfo.id, dismiss: dismiss, cleanup: cleanup) } },
theme: theme,
dismiss: dismiss,
cleanup: cleanup
@@ -1704,7 +1695,6 @@ func planAndConnect(
case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification, addressChanged, existingChat_):
if let groupSLinkData = groupSLinkData_ {
logger.debug("planAndConnect, .groupLink, .ok, short link data present")
let existingChatId = await nameChatId(filterKnownGroup == nil ? existingChat_ : nil)
await MainActor.run {
showPrepareGroupAlert(
connectionLink: connectionLink,
@@ -1715,7 +1705,7 @@ func planAndConnect(
connectOtherButton: connectOtherButton,
connectOtherLink: connectOtherLink,
addressChanged: addressChanged,
openExistingChat: existingChatId.map { chatId in { openKnownChat(chatId, dismiss: dismiss, cleanup: cleanup) } },
openExistingChat: (filterKnownGroup == nil ? existingChat_ : nil).map { chatInfo in { openKnownChat(chatInfo.id, dismiss: dismiss, cleanup: cleanup) } },
theme: theme,
dismiss: dismiss,
cleanup: cleanup
@@ -1739,16 +1729,20 @@ func planAndConnect(
await MainActor.run {
if let f = filterKnownGroup {
f(groupInfo)
if let otherSimplexName = result.otherSimplexName, let connectOtherButton {
showOtherNameAlert(otherSimplexName, connectOtherButton: connectOtherButton, theme: theme, dismiss: dismiss, cleanup: cleanup)
}
} else {
showOwnGroupLinkConfirmConnectSheet(
groupInfo: groupInfo,
connectionLink: connectionLink,
connectionPlan: connectionPlan,
connectOtherButton: connectOtherButton,
onConnectOther: connectOtherLink.map { link in { planAndConnect(link, theme: theme, dismiss: dismiss, cleanup: cleanup) } },
dismiss: dismiss,
cleanup: cleanup
)
}
showOwnGroupLinkConfirmConnectSheet(
groupInfo: groupInfo,
connectionLink: connectionLink,
connectionPlan: connectionPlan,
connectOtherButton: connectOtherButton,
onConnectOther: connectOtherLink.map { link in { planAndConnect(link, theme: theme, dismiss: dismiss, cleanup: cleanup) } },
dismiss: dismiss,
cleanup: cleanup
)
}
case .connectingConfirmReconnect:
logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect")
@@ -290,7 +290,7 @@ private suspend fun planAndConnectTask(
connectOtherButton = connectOtherButton,
connectOtherLink = connectOtherLink,
addressChanged = connectionPlan.contactAddressPlan.addressChanged,
openExistingChat = if (filterKnownContact == null) localNameChat(rhId, connectionPlan.contactAddressPlan.existingChat_)?.let { chat -> { openChat_(chatModel, rhId, close, chat); cleanup() } } else null,
openExistingChat = if (filterKnownContact == null) connectionPlan.contactAddressPlan.existingChat_?.let { chatInfo -> { openChat_(chatModel, rhId, close, Chat(remoteHostId = rhId, chatInfo = chatInfo, chatItems = emptyList())); cleanup() } } else null,
close,
cleanup
)
@@ -388,7 +388,7 @@ private suspend fun planAndConnectTask(
connectOtherButton = connectOtherButton,
connectOtherLink = connectOtherLink,
addressChanged = connectionPlan.groupLinkPlan.addressChanged,
openExistingChat = if (filterKnownGroup == null) localNameChat(rhId, connectionPlan.groupLinkPlan.existingChat_)?.let { chat -> { openChat_(chatModel, rhId, close, chat); cleanup() } } else null,
openExistingChat = if (filterKnownGroup == null) connectionPlan.groupLinkPlan.existingChat_?.let { chatInfo -> { openChat_(chatModel, rhId, close, Chat(remoteHostId = rhId, chatInfo = chatInfo, chatItems = emptyList())); cleanup() } } else null,
close,
cleanup
)
@@ -410,6 +410,7 @@ private suspend fun planAndConnectTask(
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
if (filterKnownGroup != null) {
filterKnownGroup(groupInfo)
if (otherSimplexName != null && connectOtherButton != null) showOtherNameAlert(rhId, otherSimplexName, connectOtherButton, close, cleanup)
} else {
ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, connectionPlan, groupInfo, close, cleanup, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink)
}
@@ -888,9 +889,6 @@ fun showPrepareContactAlert(
)
}
private suspend fun localNameChat(rhId: Long?, chatInfo: ChatInfo?): Chat? =
chatInfo?.let { chatModel.getChat(it.id) ?: Chat(remoteHostId = rhId, chatInfo = it, chatItems = emptyList()).also { chat -> chatModel.chatsContext.addChat(chat) } }
private fun showOtherNameAlert(rhId: Long?, otherSimplexName: SimplexNameInfo, connectOtherButton: String, close: (() -> Unit)?, cleanup: (() -> Unit)?) {
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
title = String.format(
+1 -1
View File
@@ -181,7 +181,7 @@ Review on 2026-09-24 reversed decision 6. Each decision below was taken by the a
1. **Storage.** `contact_profiles.contact_domain_resolved_at` and `contact_domain_expires_at`; `groups.group_domain_resolved_at` and `group_domain_expires_at` — beside `contact_domain_verified` and `group_domain_verified`, which record whether the name checked out; these record when it was last resolved and when its registration expires. `TEXT` in SQLite, `TIMESTAMPTZ` in Postgres, `_at` as in `badge_purchases.expires_at`. One migration per backend.
2. **Rule.** Under `PRMUnknown`, a known chat reached by a name is re-resolved when `resolved_at` is `NULL` or over a day old, or `expires_at` has passed. `PRMAll` always resolves; `PRMNever` never does. A name with no chat is resolved on every call and nothing is stored; the user's own address is not a chat, so it too is resolved on every call.
3. **Writes.** Wherever core sets a verification flag after a resolution, it also sets `resolved_at` to now and `expires_at` to the registration's expiry, or `NULL` when the caller does not have it — then only the one-day limit applies until the next resolution. That is `setContactDomainVerified`, `setGroupDomainVerified`, and `createPreparedContact`, which inserts a chat created by name already verified. The plan passes the expiry of the registration it resolved, and `updateGroupFromLinkData` takes it from its callers, since it must not resolve; `/_verify domain` and `APISetPublicGroupAccess` pass `NULL`. Two paths gain a write: a re-resolution confirming the name still resolves to the known chat, and one that answers the chat without a warning because the name no longer has a link of its kind or is not registered (`plans/2026-09-28-name-warnings.md`, N7 and N11). Both re-set the flag to `True`, a no-op, since only verified chats are found by name. Setting the flag to `True` clears it on the user's other chats of the name's kind (contacts and business chats for a contact name, channels for a channel name), so after "Open new chat" in 3c the name finds the new chat.
3. **Writes.** Wherever core sets a verification flag after a resolution, it also sets `resolved_at` to now and `expires_at` to the registration's expiry, or `NULL` when the caller does not have it — then only the one-day limit applies until the next resolution. That is `setContactDomainVerified`, `setGroupDomainVerified`, and `createPreparedContact`, which inserts a chat created by name already verified, only when the link's profile claims the name the app passes. The plan passes the expiry of the registration it resolved, and `updateGroupFromLinkData` takes it from its callers, since it must not resolve; `/_verify domain` and `APISetPublicGroupAccess` pass `NULL`. Two paths gain a write: a re-resolution confirming the name still resolves to the known chat, and one that answers the chat without a warning because the name no longer has a link of its kind or is not registered (`plans/2026-09-28-name-warnings.md`, N7 and N11). Both re-set the flag to `True`, a no-op, since only verified chats are found by name. Setting the flag to `True` clears it on the user's other chats of the name's kind (contacts and business chats for a contact name, channels for a channel name), so after "Open new chat" in 3c the name finds the new chat.
4. **Moved name.** When re-resolution finds the name resolves elsewhere (3c), nothing is written to the old chat. It stays stale, so each default lookup re-resolves and reports the new address; `resolve=never` still returns the old chat.
5. **Reading.** A store function reads the two columns for the one chat being planned. `LocalProfile` and `GroupInfo` do not change, so the columns never reach the UIs; loading them there would touch 19 queries in 6 store files and both types' JSON.
6. **UIs.** Both apps lose the cache — the preference, `SimplexNameResolved`, the local probe before resolving, and the invalidation in `UserAddressView` — and plan a name with the default mode.
+1 -1
View File
@@ -242,7 +242,7 @@ A missing `graceUntil` drops the second clause of the expiry lines. `otherSimple
- **2a, 3c, 4a:** the other kind's button is shown for bare names only.
- **3c:** also applies to the own address and channel. From a message, it shows Open new chat and Open existing chat, with no Cancel.
- **3e (new):** a bare name matches a chat, and the name also leads to the other kind: "bakery.simplex also leads to channel #bakery.simplex", Join channel, OK. From a message it also shows Open existing chat.
- **3e (new):** a bare name matches a chat or own channel, and the name also leads to the other kind: "bakery.simplex also leads to channel #bakery.simplex", Join channel, OK. From a message it also shows Open existing chat.
- **Prices:** "$X for 2 years", computed from the registry's price. The amounts on the canvas are examples.
- **3a:** unchanged: "Still leads to your chat, or not found, no valid link, another name, or the request failed" matches §3.
+7 -4
View File
@@ -2181,6 +2181,8 @@ processChatCommand cxt nm = \case
APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq
APIPrepareContact userId accLink verifiedDomain contactSLinkData -> withUserId userId $ \user -> do
let ContactShortLinkData {profile, message, business} = contactSLinkData
Profile {contactDomain} = profile
domain_ = if verifiedDomain == (claimDomain <$> contactDomain) then verifiedDomain else Nothing
welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId
case accLink of
ACCL SCMContact ccLink
@@ -2189,7 +2191,7 @@ processChatCommand cxt nm = \case
groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences
groupProfile = businessGroupProfile profile groupPreferences
gVar <- asks random
(gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing verifiedDomain
(gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing domain_
hostMember <- maybe (throwCmdError "no host member") pure hostMember_
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = CDGroupRcv gInfo Nothing hostMember
@@ -2202,7 +2204,7 @@ processChatCommand cxt nm = \case
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
ACCL _ (CCLink cReq _) -> do
ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId (True <$ verifiedDomain)
ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId (True <$ domain_)
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = CDDirectRcv ct
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing Nothing
@@ -2215,9 +2217,10 @@ processChatCommand cxt nm = \case
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTDirect chat
APIPrepareGroup userId ccLink direct verifiedDomain groupSLinkData -> withUserId userId $ \user -> do
let GroupShortLinkData {groupProfile = GroupProfile {description}} = groupSLinkData
let GroupShortLinkData {groupProfile = GroupProfile {description, publicGroup}} = groupSLinkData
domain_ = if verifiedDomain == (claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) then verifiedDomain else Nothing
welcomeSharedMsgId <- forM description $ \_ -> getSharedMsgId
(gInfo, hostMember_) <- preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId verifiedDomain
(gInfo, hostMember_) <- preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId domain_
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = maybe (CDChannelRcv gInfo Nothing) (CDGroupRcv gInfo Nothing) hostMember_
cInfo = GroupChat gInfo Nothing
+20
View File
@@ -34,6 +34,7 @@ chatNamesTests :: SpecWith TestParams
chatNamesTests = do
it "connect by resolved name" testConnectByName
it "connect by name not claimed in link profile is rejected" testConnectByNameNotClaimed
it "prepare with a name not claimed in link profile is not verified" testPrepareNameNotClaimed
it "connect by name to a known contact not claimed in profile is rejected" testConnectByNameKnownContactNotClaimed
it "connect by unregistered name reports it is available" testConnectByNameNotFound
it "set name not resolving to own address is rejected" testSetNameNotOwnAddress
@@ -447,6 +448,25 @@ setContactNamesStale cc = withCCTransaction cc $ \db -> DB.execute_ db "UPDATE c
setGroupNamesStale :: TestCC -> IO ()
setGroupNamesStale cc = withCCTransaction cc $ \db -> DB.execute_ db "UPDATE groups SET group_domain_resolved_at = datetime('now', '-2 days')"
testPrepareNameNotClaimed :: HasCallStack => TestParams -> IO ()
testPrepareNameNotClaimed ps = withSmpServerAndNames $ \reg ->
testChat2 aliceProfile bobProfile (test reg) ps
where
test reg alice bob = do
mapM_ enableNamesRole [alice, bob]
alice ##> "/ad"
(shortLink, fullLink) <- getContactLinks alice True
registerName reg aliceSimplexName (contactNameRecord "alice.simplex" (T.pack shortLink))
alice ##> "/_set domain 1 alice.simplex"
alice <## "new contact address set"
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "contact address: ok to connect"
contactSLinkData <- getTermLine bob
bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " domain=bob.simplex " <> contactSLinkData)
bob <## "alice: contact is prepared"
bob ##> "/_connect plan 1 @alice.simplex resolve=never"
bob <## "no matching chat found, name resolution is disabled"
testPlanNameReservedOther :: HasCallStack => TestParams -> IO ()
testPlanNameReservedOther = withAliceName $ \reg _r _alice bob -> do
registerReservedName reg acmeName NRRTrademark