mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-20 04:51:05 +00:00
refactor badges
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
+1
-3
@@ -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,
|
||||
|
||||
+13
-11
@@ -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 = {}) {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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())
|
||||
|
||||
+1
-3
@@ -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,
|
||||
|
||||
+1
-4
@@ -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
|
||||
|
||||
+61
-35
@@ -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 {
|
||||
|
||||
+15
-17
@@ -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),
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user