From 78de359db1cf1511bdf7b4602cf51bc5f1d85856 Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:46:37 +0000 Subject: [PATCH] refactor --- .../chat/simplex/common/model/ChatModel.kt | 3 -- .../chat/simplex/common/views/WelcomeView.kt | 2 +- .../simplex/common/views/chat/ChatInfoView.kt | 3 -- .../views/usersettings/UserProfileView.kt | 4 -- src/Simplex/Chat/Library/Internal.hs | 42 +++++++++++-------- src/Simplex/Chat/Store/Profiles.hs | 6 +-- src/Simplex/Chat/View.hs | 21 ++++++---- 7 files changed, 41 insertions(+), 40 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 6aca3007cd..c70fd319e3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1367,8 +1367,6 @@ interface NamedChat { val displayName: String val fullName: String val shortDescr: String? - // the full profile description (contacts/business/bot addresses); null for chats that have none. - // named distinctly from shortDescr and from PendingContactConnection.description (a connection-status string). val profileDescription: String? get() = null val image: String? val localAlias: String @@ -4872,7 +4870,6 @@ sealed class Format { @Serializable @SerialName("simplexName") class SimplexName(val nameInfo: SimplexNameInfo): Format() @Serializable @SerialName("command") class Command(val commandStr: String): Format() @Serializable @SerialName("mention") class Mention(val memberName: String): Format() - // app-only span (not sent over the wire): a tappable "modal" reference; the renderer resolves modalName to content in the current context @Serializable @SerialName("modal") class Modal(val modalName: String): Format() @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 1400822604..843bacecec 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -389,7 +389,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, shortDesc fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) { withBGApi { chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser( - null, Profile(displayName.trim(), "", shortDescr = null) + null, Profile(displayName.trim(), "", null, null) ) ?: return@withBGApi chatModel.localUserCreated.value = true // new users don't need the local file encryption indicator (all files are encrypted); existing users keep it on diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 5fe4d19ac1..855a87d9af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -783,9 +783,6 @@ fun ChatInfoDescription(c: NamedChat, displayName: String, copyNameToClipboard: ) } -// A compact profile description: shows shortDescr, or a truncated first line of the full description, -// with a tappable "Read more" that opens the full description in a modal. Used for contacts and, later, -// business/bot addresses. Groups keep showing only shortDescr (their welcome message is shown separately). @Composable fun ProfileDescriptionText(shortDescr: String?, description: String?, style: TextStyle, modifier: Modifier = Modifier) { val short = shortDescr?.trim()?.ifEmpty { null } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index 6fec44579b..f8bca55ab7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -76,8 +76,6 @@ fun UserProfileLayout( var savedKeyboardState by remember { mutableStateOf(keyboardState) } val focusRequester = remember { FocusRequester() } val descrFocusRequester = remember { FocusRequester() } - // the description is edited on a separate screen shown in place of the profile form (same composition, - // so the edited value survives). It is persisted together with the rest of the profile by the save button. var editingDescription by remember { mutableStateOf(false) } ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), @@ -195,8 +193,6 @@ fun UserProfileLayout( ProfileNameField(shortDescr) Spacer(Modifier.height(DEFAULT_PADDING)) - // shows the description editor in place of this form; the edited value is saved together - // with the rest of the profile by the "Save and notify contacts" button below. Text( stringResource(if (description.value.isBlank()) MR.strings.add_description else MR.strings.edit_description), color = MaterialTheme.colors.primary, diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 3ecd50c65b..6ed70f886d 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1265,35 +1265,41 @@ memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, a redactedMemberProfile :: GroupInfo -> GroupMember -> Profile -> Profile redactedMemberProfile g m Profile {displayName, fullName, shortDescr, description, image, contactLink = lnk, peerType, badge, contactDomain} = - Profile {displayName, fullName, shortDescr = removeSimplexLink =<< shortDescr, description = redactGroupDescription g m =<< description, image, contactLink, preferences = Nothing, peerType, badge, contactDomain = redactedDomain} + Profile {displayName, fullName, shortDescr = redactLinkedText dropWholeOnLink allowSimplexLinks =<< shortDescr, description = redactGroupDescription g m =<< description, image, contactLink, preferences = Nothing, peerType, badge, contactDomain = redactedDomain} where contactLink = if allowSimplexLinks then lnk else Nothing redactedDomain = if allowDirect then (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain else Nothing allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect - removeSimplexLink s - | allowSimplexLinks = Just s - | hasObfuscatedSimplexLink s = Nothing - | otherwise = maybe (Just s) (\fts -> if any ftIsSimplexLink fts then Nothing else Just s) $ parseMaybeMarkdownList s + allowSimplexLinks = memberLinksAllowed g m --- redact only the description of an incoming member profile (receive side), per the group's link/name policy redactMemberProfileDescription :: GroupInfo -> GroupMember -> Profile -> Profile -redactMemberProfileDescription g m p@Profile {description} = p {description = redactGroupDescription g m =<< description} +redactMemberProfileDescription g m p@Profile {description} = + p {description = redactGroupDescription g m =<< description} --- strip link/name spans inline from a group member's description (keeping the prose), dropping the whole field only on an obfuscated link redactGroupDescription :: GroupInfo -> GroupMember -> Text -> Maybe Text -redactGroupDescription g m s +redactGroupDescription g m = redactLinkedText stripLinkSpans (memberLinksAllowed g m) + +memberLinksAllowed :: GroupInfo -> GroupMember -> Bool +memberLinksAllowed g m = groupFeatureMemberAllowed SGFSimplexLinks m g && groupFeatureMemberAllowed SGFDirectMessages m g + +redactLinkedText :: (Text -> [FormattedText] -> Maybe Text) -> Bool -> Text -> Maybe Text +redactLinkedText fromSpans allowSimplexLinks s | allowSimplexLinks = Just s | hasObfuscatedSimplexLink s = Nothing - | otherwise = case parseMaybeMarkdownList s of - Nothing -> Just s - Just fts -> - let kept = T.concat [t | FormattedText f t <- fts, not (maybe False isLinkOrName f)] - in if T.null (T.strip kept) then Nothing else Just kept + | otherwise = maybe (Just s) (fromSpans s) $ parseMaybeMarkdownList s + +dropWholeOnLink :: Text -> [FormattedText] -> Maybe Text +dropWholeOnLink s fts + | any ftIsSimplexLink fts = Nothing + | otherwise = Just s + +stripLinkSpans :: Text -> [FormattedText] -> Maybe Text +stripLinkSpans _ fts + | T.null (T.strip kept) = Nothing + | otherwise = Just kept where - allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect - isLinkOrName f = isLink f || case f of Mention {} -> True; _ -> False + kept = T.concat [t | FormattedText f t <- fts, not (maybe False linkOrMention f)] + linkOrMention f = isLink f || case f of Mention {} -> True; _ -> False -- Roles carried by the roster; owners are on the link, not the roster. isRosterRole :: GroupMemberRole -> Bool diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 139dbbe2b2..19120bca27 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -353,9 +353,9 @@ updateUserProfile db user p' DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId) pure $ Just currentTs | otherwise = pure userMemberProfileUpdatedAt - userMemberProfileChanged = newName /= displayName || fn' /= fullName || d' /= shortDescr || img' /= image - User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, localBadge, localAlias}, userMemberProfileUpdatedAt} = user - Profile {displayName = newName, fullName = fn', shortDescr = d', image = img', preferences} = p' + userMemberProfileChanged = newName /= displayName || fn' /= fullName || d' /= shortDescr || desc' /= description || img' /= image + User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, localBadge, localAlias}, userMemberProfileUpdatedAt} = user + Profile {displayName = newName, fullName = fn', shortDescr = d', description = desc', image = img', preferences} = p' fullPreferences = fullPreferences' preferences -- own profile field update; leaves the badge columns alone (the credential is owned by setUserBadge/addUserBadge) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 5b90f492f6..74b89138e5 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1959,14 +1959,15 @@ viewSwitchPhase = \case SPCompleted -> "changed address" viewUserProfileUpdated :: Profile -> Profile -> UserProfileUpdateSummary -> [StyledString] -viewUserProfileUpdated Profile {displayName = n, fullName, shortDescr, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', shortDescr = shortDescr', image = image', contactLink = contactLink', preferences = prefs'} summary = +viewUserProfileUpdated Profile {displayName = n, fullName, shortDescr, description, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', shortDescr = shortDescr', description = description', image = image', contactLink = contactLink', preferences = prefs'} summary = profileUpdated <> viewPrefsUpdated preferences prefs' where UserProfileUpdateSummary {updateSuccesses = s, updateFailures = f} = summary profileUpdated - | n == n' && fullName == fullName' && shortDescr == shortDescr' && image == image' && contactLink == contactLink' = [] - | n == n' && fullName == fullName' && shortDescr == shortDescr' && image == image' = [if isNothing contactLink' then "contact address removed" else "new contact address set"] - | n == n' && fullName == fullName' && shortDescr == shortDescr' = [if isNothing image' then "profile image removed" else "profile image updated"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && image == image' && contactLink == contactLink' = [] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && image == image' = [if isNothing contactLink' then "contact address removed" else "new contact address set"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' = [if isNothing image' then "profile image removed" else "profile image updated"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' = ["user description " <> (if maybe True T.null description' then "removed" else "changed to " <> maybe "" plain description') <> notified] | n == n' && fullName == fullName' = ["user bio " <> (if maybe True T.null shortDescr' then "removed" else "changed to " <> maybe "" plain shortDescr') <> notified] | n == n' = ["user full name " <> (if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName') <> notified] | otherwise = ["user profile is changed to " <> ttyFullName n' fullName' shortDescr' <> notified] @@ -2257,13 +2258,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated - Contact {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, contactLink}} - Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName', shortDescr = shortDescr', contactLink = contactLink'}} - | n == n' && fullName == fullName' && shortDescr == shortDescr' && contactLink == contactLink' = [] - | n == n' && fullName == fullName' && shortDescr == shortDescr' = + Contact {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, description, contactLink}} + Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName', shortDescr = shortDescr', description = description', contactLink = contactLink'}} + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && contactLink == contactLink' = [] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' = if isNothing contactLink' then [ttyContact n <> " removed contact address"] else [ttyContact n <> " set new contact address, use " <> highlight ("/info " <> n) <> " to view"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' = + if maybe True T.null description' + then ["contact " <> ttyContact n <> " removed description"] + else ["contact " <> ttyContact n <> " updated description: " <> maybe "" plain description'] | n == n' && fullName == fullName' = if maybe True T.null shortDescr' then ["contact " <> ttyContact n <> " removed bio"]