refactor badges

This commit is contained in:
Evgeny @ SimpleX Chat
2026-06-11 20:51:22 +00:00
parent 409028ad4a
commit d9aeb64895
14 changed files with 123 additions and 117 deletions
@@ -1555,7 +1555,7 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
if (cInfo.incognito) {
IncognitoImage(size = 36.dp * fontSizeSqrtMultiplier, Indigo)
}
ChatInfoImage(cInfo, size = imageSize * fontSizeSqrtMultiplier, iconColor)
ChatInfoImage(cInfo, size = imageSize, iconColor, scaled = true)
Column(
Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -2793,7 +2793,7 @@ val MEMBER_IMAGE_SIZE: Dp = 37.dp
@Composable
fun MemberImage(member: GroupMember) {
MemberProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member, backgroundColor = MaterialTheme.colors.background)
MemberProfileImage(MEMBER_IMAGE_SIZE, member, backgroundColor = MaterialTheme.colors.background, scaled = true)
}
@Composable
@@ -352,9 +352,7 @@ fun ContactCheckRow(
}
} else null
) {
BadgedProfileImage(36.dp, if (contact.active) contact.profile.localBadge else null) {
ProfileImage(size = 36.dp, contact.image)
}
ProfileImage(size = 36.dp, contact.image, badge = if (contact.active) contact.profile.localBadge else null)
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
Text(
contact.chatViewName,
@@ -901,19 +901,21 @@ fun MemberProfileImage(
color: Color = MaterialTheme.colors.secondaryVariant,
backgroundColor: Color? = null,
async: Boolean = false,
tappableBadge: Boolean = false
tappableBadge: Boolean = false,
scaled: Boolean = false
) {
val badge = mem.memberProfile.localBadge
BadgedProfileImage(size, badge, onBadgeClick = if (tappableBadge) badge?.let { b -> { showBadgeInfoAlert(b) } } else null) {
ProfileImage(
size = size,
image = mem.image,
color = color,
backgroundColor = backgroundColor,
blurred = mem.blocked,
async = async
)
}
ProfileImage(
size = size,
image = mem.image,
color = color,
backgroundColor = backgroundColor,
blurred = mem.blocked,
async = async,
badge = badge,
onBadgeClick = if (tappableBadge) badge?.let { b -> { showBadgeInfoAlert(b) } } else null,
scaled = scaled
)
}
fun updateMembersRole(newRole: GroupMemberRole, rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onFailure: () -> Unit = {}, onSuccess: () -> Unit = {}) {
@@ -128,7 +128,7 @@ fun MemberSupportChatToolbarTitle(member: GroupMember, imageSize: Dp = 40.dp, ic
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
MemberProfileImage(size = imageSize * fontSizeSqrtMultiplier, member, iconColor)
MemberProfileImage(size = imageSize, member, iconColor, scaled = true)
Column(
Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -389,7 +389,7 @@ fun ChatPreviewView(
Box(contentAlignment = Alignment.Center) {
Row {
Box(contentAlignment = Alignment.BottomEnd) {
ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier, tappableBadge = true)
ChatInfoImage(cInfo, size = 72.dp, tappableBadge = true, scaled = true)
Box(Modifier.padding(end = 6.sp.toDp(), bottom = 6.sp.toDp())) {
chatPreviewImageOverlayIcon()
}
@@ -21,7 +21,7 @@ import chat.simplex.res.MR
@Composable
fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) {
Row {
ChatInfoImage(contactRequest, size = 72.dp * fontSizeSqrtMultiplier)
ChatInfoImage(contactRequest, size = 72.dp, scaled = true)
Column(
modifier = Modifier
.padding(start = 8.dp, end = 8.sp.toDp())
@@ -103,9 +103,7 @@ private fun SharePreviewView(chat: Chat, disabled: Boolean) {
ProfileImage(size = 42.dp, chat.chatInfo.image, icon = MR.images.ic_supervised_user_circle_filled)
} else {
val ct = (chat.chatInfo as? ChatInfo.Direct)?.contact
BadgedProfileImage(42.dp, if (ct?.active == true) ct.profile.localBadge else null) {
ProfileImage(size = 42.dp, chat.chatInfo.image)
}
ProfileImage(size = 42.dp, chat.chatInfo.image, badge = if (ct?.active == true) ct.profile.localBadge else null)
}
Text(
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
@@ -464,10 +464,7 @@ fun UserProfileRow(u: User, enabled: Boolean = remember { chatModel.chatRunning
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
val avatarSize = 54.dp * fontSizeSqrtMultiplier
BadgedProfileImage(avatarSize, u.profile.localBadge) {
ProfileImage(image = u.image, size = avatarSize)
}
ProfileImage(image = u.image, size = 54.dp, badge = u.profile.localBadge, scaled = true)
Text(
u.displayName,
modifier = Modifier
@@ -28,9 +28,10 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import kotlin.math.max
import kotlin.math.roundToInt
@Composable
fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false, tappableBadge: Boolean = false) {
fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false, tappableBadge: Boolean = false, scaled: Boolean = false) {
val icon =
when (chatInfo) {
is ChatInfo.Group -> chatInfo.groupInfo.chatIconName
@@ -44,9 +45,15 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme
chatInfo is ChatInfo.ContactRequest -> chatInfo.contactRequest.profile.localBadge
else -> null
}
BadgedProfileImage(size, badge, onBadgeClick = if (tappableBadge) badge?.let { b -> { showBadgeInfoAlert(b) } } else null) {
ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor)
}
ProfileImage(
size,
chatInfo.image,
icon,
if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor,
badge = badge,
onBadgeClick = if (tappableBadge) badge?.let { b -> { showBadgeInfoAlert(b) } } else null,
scaled = scaled
)
}
@Composable
@@ -60,6 +67,9 @@ fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVa
}
}
// `size` is the design size; `scaled = true` renders the avatar (and badge) at size * fontSizeSqrtMultiplier.
// The badge is measured but only the avatar's size is reported, so it overflows bottom-right
// and has no effect on the avatar's own layout or anything around it.
@Composable
fun ProfileImage(
size: Dp,
@@ -68,8 +78,33 @@ fun ProfileImage(
color: Color = MaterialTheme.colors.secondaryVariant,
backgroundColor: Color? = null,
blurred: Boolean = false,
async: Boolean = false
async: Boolean = false,
badge: LocalBadge? = null,
onBadgeClick: (() -> Unit)? = null,
scaled: Boolean = false
) {
val sz = if (scaled) size * fontSizeSqrtMultiplier else size
if (badge == null) {
ProfileImageBox(sz, image, icon, color, backgroundColor, blurred, async)
} else {
Layout(content = {
ProfileImageBox(sz, image, icon, color, backgroundColor, blurred, async)
ProfileBadge(badgeWidthRatio(size) * sz, badge, onBadgeClick)
}) { measurables, constraints ->
val avatar = measurables[0].measure(constraints)
val bdg = measurables[1].measure(Constraints())
layout(avatar.width, avatar.height) {
avatar.place(0, 0)
// badge center sits 0.33 * size right and down of the avatar center
val off = (0.33f * avatar.width).toInt()
bdg.place(x = avatar.width / 2 + off - bdg.width / 2, y = avatar.height / 2 + off - bdg.height / 2)
}
}
}
}
@Composable
private fun ProfileImageBox(size: Dp, image: String?, icon: ImageResource, color: Color, backgroundColor: Color?, blurred: Boolean, async: Boolean) {
Box(Modifier.size(size)) {
if (image == null) {
val iconToReplace = when (icon) {
@@ -116,29 +151,6 @@ fun ProfileImage(
}
}
// Overlays a supporter badge on an avatar with zero layout impact: a custom Layout measures the badge but
// reports only the avatar's size, so the badge overflows bottom-right (not clamped) and nothing around it shifts.
@Composable
fun BadgedProfileImage(size: Dp, badge: LocalBadge?, onBadgeClick: (() -> Unit)? = null, avatar: @Composable () -> Unit) {
if (badge == null) {
avatar()
return
}
Layout(content = {
avatar()
ProfileBadge(size, badge, onBadgeClick)
}) { measurables, constraints ->
val a = measurables[0].measure(constraints)
val b = measurables[1].measure(Constraints())
layout(a.width, a.height) {
a.place(0, 0)
// phone center sits 0.33*S right and down of the avatar center, overflowing the avatar bounds
val off = (0.33f * a.width).toInt()
b.place(x = a.width / 2 + off - b.width / 2, y = a.height / 2 + off - b.height / 2)
}
}
}
// tones the glyph down: slightly desaturated and dimmed, so the gradient is less bright against the avatar
private val badgeColorFilter = ColorFilter.colorMatrix(
ColorMatrix().apply {
@@ -156,14 +168,28 @@ private val badgeColorFilter = ColorFilter.colorMatrix(
}
)
// the phone glyph (or warning triangle) scales inversely to the avatar so it stays readable when the avatar is small.
// Badge width as a fraction of the avatar design size, one entry per size in use - tune each placement here.
// Values match the original inverse-scaling formula 0.2133 * (1 + 0.5 * clamp((192 - s) / 156, 0, 1)):
// the smaller the avatar, the bigger the badge relative to it, so it stays readable at small sizes.
private fun badgeWidthRatio(size: Dp): Float = when (size.value.roundToInt()) {
36 -> 0.32f // AddGroupMembersView (contact rows when adding members), ChatItemInfoView (member row in message info)
37 -> 0.3192f // MEMBER_IMAGE_SIZE - member avatar next to group message bubbles
38 -> 0.3185f // ChannelMembersView, ChannelRelaysView (member rows)
40 -> 0.3172f // ChatInfoToolbarTitle (chat header), MemberSupportChatToolbarTitle (member support chat header)
42 -> 0.3158f // ContactPreviewView (contacts list), ShareListNavLinkView (share-to list), MEMBER_ROW_AVATAR_SIZE (group info / member support member rows)
54 -> 0.3076f // UserPicker (user switcher)
57 -> 0.3056f // ChatItemInfoView (forwarded-from chat)
60 -> 0.3035f // AddGroupMembersView toolbar (group avatar - shows no badge currently)
72 -> 0.2953f // ChatPreviewView (chat list), ContactRequestView (contact request in chat list)
138 -> 0.2502f // alertProfileImageSize - open-chat and scan-link alerts
192 -> 0.2133f // ChatInfoView (contact info), GroupMemberInfoView (member info)
else -> 0.2133f
}
@Composable
private fun ProfileBadge(size: Dp, badge: LocalBadge, onBadgeClick: (() -> Unit)?) {
val s = size.value
val mult = 1f + 0.5f * ((192f - s) / 156f).coerceIn(0f, 1f)
// phone height ~0.28*S; set width only (= 0.7617*height) and let the height follow the glyph's aspect ratio
val phoneW = 0.28f * 0.7617f * size * mult
val mod = Modifier.width(phoneW).let { if (onBadgeClick != null) it.clickable(onClick = onBadgeClick) else it }
private fun ProfileBadge(width: Dp, badge: LocalBadge, onBadgeClick: (() -> Unit)?) {
// width only - the height follows the glyph's intrinsic aspect ratio
val mod = Modifier.width(width).let { if (onBadgeClick != null) it.clickable(onClick = onBadgeClick) else it }
if (badge.status == BadgeStatus.Failed) {
Icon(painterResource(MR.images.ic_warning_filled), contentDescription = null, tint = WarningOrange, modifier = mod)
} else {
@@ -468,13 +468,12 @@ private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close:
profileName = contact.profile.displayName,
profileFullName = contact.profile.fullName,
profileImage = {
BadgedProfileImage(alertProfileImageSize, if (contact.active) contact.profile.localBadge else null) {
ProfileImage(
size = alertProfileImageSize,
image = contact.profile.image,
icon = contact.chatIconName
)
}
ProfileImage(
size = alertProfileImageSize,
image = contact.profile.image,
icon = contact.chatIconName,
badge = if (contact.active) contact.profile.localBadge else null
)
},
confirmText = generalGetString(if (contact.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat),
onConfirm = {
@@ -626,16 +625,15 @@ fun showPrepareContactAlert(
profileName = contactShortLinkData.profile.displayName,
profileFullName = contactShortLinkData.profile.fullName,
profileImage = {
BadgedProfileImage(alertProfileImageSize, contactShortLinkData.localBadge) {
ProfileImage(
size = alertProfileImageSize,
image = contactShortLinkData.profile.image,
icon =
if (contactShortLinkData.business) MR.images.ic_work_filled_padded
else if (contactShortLinkData.profile.peerType == ChatPeerType.Bot) MR.images.ic_cube
else MR.images.ic_account_circle_filled
)
}
ProfileImage(
size = alertProfileImageSize,
image = contactShortLinkData.profile.image,
icon =
if (contactShortLinkData.business) MR.images.ic_work_filled_padded
else if (contactShortLinkData.profile.peerType == ChatPeerType.Bot) MR.images.ic_cube
else MR.images.ic_account_circle_filled,
badge = contactShortLinkData.localBadge
)
},
information = ownerVerificationMessage(ownerVerification),
confirmText = generalGetString(MR.strings.connect_plan_open_new_chat),
+11 -11
View File
@@ -1942,7 +1942,7 @@ processChatCommand cxt nm = \case
-- [incognito] generate profile for connection
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode
linkProfile <- presentUserBadge' incognitoProfile user $ userProfileDirect user incognitoProfile Nothing True
linkProfile <- presentUserBadge user incognitoProfile $ userProfileDirect user incognitoProfile Nothing True
let userData = contactShortLinkData linkProfile Nothing
userLinkData = UserInvLinkData userData
-- TODO [certs rcv]
@@ -1965,7 +1965,7 @@ processChatCommand cxt nm = \case
updatePCCIncognito db user conn (Just pId) sLnk
pure $ CRConnectionIncognitoUpdated user conn' (Just incognitoProfile)
(ConnNew, Just pId, False) -> do
sLnk <- updatePCCShortLinkData conn =<< presentUserBadge user (userProfileDirect user Nothing Nothing True)
sLnk <- updatePCCShortLinkData conn =<< presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True)
conn' <- withFastStore' $ \db -> do
deletePCCIncognitoProfile db user pId
updatePCCIncognito db user conn Nothing sLnk
@@ -1986,7 +1986,7 @@ processChatCommand cxt nm = \case
let short = isJust $ connShortLink' =<< connLinkInv
userLinkData_ <-
if short
then Just . UserInvLinkData . (`contactShortLinkData` Nothing) <$> presentUserBadge newUser (userProfileDirect newUser Nothing Nothing True)
then Just . UserInvLinkData . (`contactShortLinkData` Nothing) <$> presentUserBadge newUser Nothing (userProfileDirect newUser Nothing Nothing True)
else pure Nothing
-- TODO [certs rcv]
(agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation userLinkData_ Nothing IKPQOn subMode
@@ -2265,7 +2265,7 @@ processChatCommand cxt nm = \case
userData <-
if isTrue userChatRelay
then pure $ relayShortLinkData (userProfileDirect user Nothing Nothing True)
else (`contactShortLinkData` Nothing) <$> presentUserBadge user (userProfileDirect user Nothing Nothing True)
else (`contactShortLinkData` Nothing) <$> presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True)
let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
-- TODO [certs rcv]
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) Nothing IKPQOn subMode
@@ -3147,7 +3147,7 @@ processChatCommand cxt nm = \case
joinPreparedConn subMode conn
joinPreparedConn subMode conn = do
-- [incognito] send membership incognito profile
p <- presentUserBadge' (incognitoMembershipProfile gInfo) user $ userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile gInfo) Nothing True
p <- presentUserBadge user (incognitoMembershipProfile gInfo) $ userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile gInfo) Nothing True
dm <- encodeConnInfo $ XInfo p
(sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode
let newStatus = if sqSecured then ConnSndReady else ConnJoined
@@ -3525,7 +3525,7 @@ processChatCommand cxt nm = \case
conn <- withFastStore' $ \db -> createDirectConnection' db userId connId ccLink contactId_ ConnPrepared incognitoProfile subMode chatV pqSup'
joinPreparedConn conn incognitoProfile chatV
joinPreparedConn conn incognitoProfile chatV = do
profileToSend <- presentUserBadge' incognitoProfile user $ userProfileDirect user incognitoProfile Nothing True
profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user incognitoProfile Nothing True
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
(sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup' subMode
let newStatus = if sqSecured then ConnSndReady else ConnJoined
@@ -3673,7 +3673,7 @@ processChatCommand cxt nm = \case
joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ pqSup = do
-- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile
profileToSend <-
presentUserBadge' incognitoProfile user $ case gInfo_ of
presentUserBadge user incognitoProfile $ case gInfo_ of
Just gInfo_' ->
let allowSimplexLinks = maybe True (groupFeatureUserAllowed SGFSimplexLinks) gInfo_'
in userProfileInGroup' user allowSimplexLinks incognitoProfile
@@ -3756,7 +3756,7 @@ processChatCommand cxt nm = \case
-- non-incognito (filtered above), so the user's badge is presented; a profile update keeps the badge instead of clearing it
ctSndEvent :: ChangedProfileContact -> CM (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json)
ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = do
p <- presentUserBadge user' mergedProfile'
p <- presentUserBadge user' Nothing mergedProfile'
pure (ConnectionId connId, Nothing, XInfo p)
ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq
ctMsgReq ChangedProfileContact {conn} =
@@ -3765,7 +3765,7 @@ processChatCommand cxt nm = \case
setMyAddressData :: User -> UserContactLink -> CM UserContactLink
setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _sLnk_, addressSettings} = do
conn <- withFastStore $ \db -> getUserAddressConnection db cxt user
shortLinkProfile <- presentUserBadge user $ userProfileDirect user Nothing Nothing True
shortLinkProfile <- presentUserBadge user Nothing $ userProfileDirect user Nothing Nothing True
-- TODO [short links] do not save address to server if data did not change, spinners, error handling
let userData
| isTrue userChatRelay = relayShortLinkData shortLinkProfile
@@ -3788,7 +3788,7 @@ processChatCommand cxt nm = \case
mergedProfile' = userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') False
when (mergedProfile' /= mergedProfile) $
withContactLock "updateContactPrefs" (contactId' ct) $ do
p <- presentUserBadge' incognitoProfile user mergedProfile'
p <- presentUserBadge user incognitoProfile mergedProfile'
void (sendDirectContactMessage user ct' $ XInfo p) `catchAllErrors` eToView
lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct'
pure $ CRContactPrefsUpdated user ct ct'
@@ -4668,7 +4668,7 @@ addUserBadge user cred@(BadgeCredential _ _ info) = do
Right conn
| not (connIncognito conn) -> do
let ct' = updateMergedPreferences user' ct
p <- presentUserBadge user' $ userProfileDirect user' Nothing (Just ct') False
p <- presentUserBadge user' Nothing $ userProfileDirect user' Nothing (Just ct') False
void (sendDirectContactMessage user' ct' (XInfo p)) `catchAllErrors` eToView
_ -> pure ()
+7 -13
View File
@@ -907,7 +907,7 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId
Just conn@Connection {customUserProfileId} -> do
incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
pure (ct, conn, ExistingIncognito <$> incognitoProfile)
profileToSend <- presentUserBadge' incognitoProfile user $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True
profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
-- TODO [certs rcv]
(ct,conn,) . fst <$> withAgent (\a -> acceptContact a nm (aUserId user) (aConnId conn) True invId dm pqSup' subMode)
@@ -920,7 +920,7 @@ acceptContactRequestAsync
UserContactRequest {agentInvitationId = AgentInvId cReqInvId, cReqChatVRange, xContactId, pqSupport = cReqPQSup}
incognitoProfile = do
subMode <- chatReadVar subscriptionMode
profileToSend <- presentUserBadge' incognitoProfile user $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True
profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True
cxt <- chatStoreCxt
let chatV = vr cxt `peerConnChatVersion` cReqChatVRange
(cmdId, acId) <- agentAcceptContactAsync user True cReqInvId (XInfo profileToSend) subMode cReqPQSup chatV
@@ -1897,22 +1897,16 @@ sendDirectContactMessages' user ct events = do
pure sndMsgs'
-- present the user's own badge on an outgoing profile: a fresh, single-use proof from the stored credential.
-- only own credentials present (peers carry proofs, which are not re-presented). callers must not present on incognito sends.
presentUserBadge :: User -> Profile -> CM Profile
presentUserBadge User {profile = LocalProfile {localBadge}} p = case localBadge of
Just (OwnBadge cred _) -> do
-- the send's incognito profile (when set) suppresses it - an incognito identity must never carry the badge.
presentUserBadge :: User -> Maybe i -> Profile -> CM Profile
presentUserBadge User {profile = LocalProfile {localBadge}} incognitoProfile p = case (incognitoProfile, localBadge) of
(Nothing, Just (OwnBadge cred _)) -> do
key <- asks $ badgePublicKey . config
liftIO (badgeProof key cred PHTest) >>= \case
Right proof -> pure p {badge = Just proof}
Left e -> p <$ logError ("presentUserBadge: proof generation failed: " <> T.pack e)
_ -> pure p
-- gated on the send's incognito profile: an incognito identity must never carry the user's badge
presentUserBadge' :: Maybe a -> User -> Profile -> CM Profile
presentUserBadge' incognitoProfile user p = case incognitoProfile of
Just _ -> pure p
Nothing -> presentUserBadge user p
-- receiving side of contact/invitation link data: verify the badge proof from the link profile
-- and set the crypto-free display badge for the UI (the raw proof stays in profile for APIPrepareContact)
linkDataBadge :: ContactShortLinkData -> CM ContactShortLinkData
@@ -2132,7 +2126,7 @@ sendGroupMessages user gInfo scope asGroup members events = do
let members' = filter (`supportsVersion` memberProfileUpdateVersion) members
allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
-- shouldSendProfileUpdate excludes incognito membership, so the badge is presented
profileUpdate <- presentUserBadge user $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile p
profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile p
void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate
currentTs <- liftIO getCurrentTime
withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs
+7 -7
View File
@@ -438,7 +438,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
(conn'', gInfo_) <- saveConnInfo conn' connInfo
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
profileToSend <-
presentUserBadge' incognitoProfile user $ case gInfo_ of
presentUserBadge user incognitoProfile $ case gInfo_ of
Just gInfo -> userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
Nothing -> userProfileDirect user (fromLocalProfile <$> incognitoProfile) Nothing True
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
@@ -556,7 +556,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
ct' <- processContactProfileUpdate ct profile False `catchAllErrors` const (pure ct)
-- [incognito] send incognito profile
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId
p <- presentUserBadge' incognitoProfile user $ userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') True
p <- presentUserBadge user incognitoProfile $ userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') True
allowAgentConnectionAsync user conn'' confId $ XInfo p
void $ withStore' $ \db -> resetMemberContactFields db ct'
XGrpLinkInv glInv -> do
@@ -567,7 +567,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart)
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId)
profileToSend <- presentUserBadge' incognitoProfile user $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend
toView $ CEvtBusinessLinkConnecting user gInfo host ct
_ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info"
@@ -799,7 +799,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
(gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db cxt user gInfo m glInv
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId)
profileToSend <- presentUserBadge' incognitoProfile user $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
allowAgentConnectionAsync user conn' confId $ XInfo profileToSend
toView $ CEvtGroupLinkConnecting user gInfo' m'
| otherwise -> messageError "x.grp.link.inv: publicGroupId mismatch"
@@ -814,7 +814,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
| sameMemberId memId m -> do
let GroupMember {memberId = membershipMemId} = membership
allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
membershipProfile <- presentUserBadge' (incognitoMembershipProfile gInfo) user $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
-- TODO update member profile
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
allowAgentConnectionAsync user conn' confId $ XGrpMemInfo membershipMemId membershipProfile
@@ -922,7 +922,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
where
sendXGrpLinkMem gInfo'' = do
let incognitoProfile = ExistingIncognito <$> incognitoMembershipProfile gInfo''
profileToSend <- presentUserBadge' incognitoProfile user $ userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile)
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile)
void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend) groupId
_ -> do
unless (memberPending m) $ withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected
@@ -3087,7 +3087,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
subMode <- chatReadVar subscriptionMode
-- [incognito] send membership incognito profile, create direct connection as incognito
let allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
membershipProfile <- presentUserBadge' (incognitoMembershipProfile gInfo) user $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile
-- [async agent commands] no continuation needed, but commands should be asynchronous for stability
groupConnIds <- joinAgentConnectionAsync user Nothing (chatHasNtfs chatSettings) groupConnReq dm subMode
+1 -8
View File
@@ -27,7 +27,7 @@ import Data.Maybe (isNothing)
import qualified Data.Text as T
import Network.Socket
import Simplex.Chat
import Simplex.Chat.Controller (CM, ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg)
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg)
import Simplex.Chat.Core
import Simplex.Chat.Library.Commands
import Simplex.Chat.Options
@@ -454,13 +454,6 @@ userName :: TestCC -> IO [Char]
userName (TestCC ChatController {currentUser} _ _ _ _ _) =
maybe "no current user" (\User {localDisplayName} -> T.unpack localDisplayName) <$> readTVarIO currentUser
-- run an internal CM action against a test client's controller and active user (for functions with no command surface)
runCCUser :: HasCallStack => TestCC -> (User -> CM a) -> IO a
runCCUser TestCC {chatController = cc@ChatController {currentUser}} action =
readTVarIO currentUser >>= \case
Just user -> runReaderT (runExceptT (action user)) cc >>= either (error . show) pure
Nothing -> error "withCCUser: no current user"
testChat :: HasCallStack => Profile -> (HasCallStack => TestCC -> IO ()) -> TestParams -> IO ()
testChat = testChatCfgOpts testCfg testOpts