mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-20 04:51:05 +00:00
update UI, tests
This commit is contained in:
+9
-4
@@ -1994,7 +1994,10 @@ data class Profile(
|
||||
override val localAlias : String = "",
|
||||
val contactLink: String? = null,
|
||||
val preferences: ChatPreferences? = null,
|
||||
val peerType: ChatPeerType? = null
|
||||
val peerType: ChatPeerType? = null,
|
||||
// opaque badge proof from the wire profile: not interpreted by the UI (display uses crypto-free LocalBadge),
|
||||
// but preserved so passing a link profile back to the core (apiPrepareContact) keeps the proof
|
||||
val badge: JsonElement? = null
|
||||
): NamedChat {
|
||||
val profileViewName: String
|
||||
get() {
|
||||
@@ -2330,7 +2333,9 @@ enum class MemberCriteria {
|
||||
data class ContactShortLinkData (
|
||||
val profile: Profile,
|
||||
val message: MsgContent?,
|
||||
val business: Boolean
|
||||
val business: Boolean,
|
||||
// set by the core when building the connection plan: the link profile's badge, verified and crypto-free
|
||||
val localBadge: LocalBadge? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -2779,7 +2784,7 @@ class UserContactRequest (
|
||||
val contactRequestId: Long,
|
||||
val cReqChatVRange: VersionRange,
|
||||
override val localDisplayName: String,
|
||||
val profile: Profile,
|
||||
val profile: LocalProfile,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -2805,7 +2810,7 @@ class UserContactRequest (
|
||||
contactRequestId = 1,
|
||||
cReqChatVRange = VersionRange(1, 1),
|
||||
localDisplayName = "alice",
|
||||
profile = Profile.sampleData,
|
||||
profile = LocalProfile.sampleData,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
+2
-1
@@ -2051,7 +2051,8 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
|
||||
val member = cItem.chatDir.groupMember
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
// zIndex draws the avatar (and its badge overflow) above the message bubble tail
|
||||
Box(Modifier.zIndex(1f).clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
MemberImage(member)
|
||||
}
|
||||
Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
|
||||
|
||||
+3
-1
@@ -352,7 +352,9 @@ fun ContactCheckRow(
|
||||
}
|
||||
} else null
|
||||
) {
|
||||
ProfileImage(size = 36.dp, contact.image)
|
||||
BadgedProfileImage(36.dp, if (contact.active) contact.profile.localBadge else null) {
|
||||
ProfileImage(size = 36.dp, contact.image)
|
||||
}
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
Text(
|
||||
contact.chatViewName,
|
||||
|
||||
+1
-1
@@ -389,7 +389,7 @@ fun ChatPreviewView(
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Row {
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier)
|
||||
ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier, tappableBadge = true)
|
||||
Box(Modifier.padding(end = 6.sp.toDp(), bottom = 6.sp.toDp())) {
|
||||
chatPreviewImageOverlayIcon()
|
||||
}
|
||||
|
||||
+4
-1
@@ -102,7 +102,10 @@ private fun SharePreviewView(chat: Chat, disabled: Boolean) {
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
ProfileImage(size = 42.dp, chat.chatInfo.image, icon = MR.images.ic_supervised_user_circle_filled)
|
||||
} else {
|
||||
ProfileImage(size = 42.dp, chat.chatInfo.image)
|
||||
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)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
|
||||
+6
-1
@@ -38,7 +38,12 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme
|
||||
is ChatInfo.Direct -> chatInfo.contact.chatIconName
|
||||
else -> MR.images.ic_account_circle_filled
|
||||
}
|
||||
val badge = if (chatInfo is ChatInfo.Direct) chatInfo.contact.profile.localBadge else null
|
||||
// a deleted (inactive) contact shows the inactive icon instead of the badge
|
||||
val badge = when {
|
||||
chatInfo is ChatInfo.Direct && chatInfo.contact.active -> chatInfo.contact.profile.localBadge
|
||||
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)
|
||||
}
|
||||
|
||||
+17
-13
@@ -468,11 +468,13 @@ private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close:
|
||||
profileName = contact.profile.displayName,
|
||||
profileFullName = contact.profile.fullName,
|
||||
profileImage = {
|
||||
ProfileImage(
|
||||
size = alertProfileImageSize,
|
||||
image = contact.profile.image,
|
||||
icon = contact.chatIconName
|
||||
)
|
||||
BadgedProfileImage(alertProfileImageSize, if (contact.active) contact.profile.localBadge else null) {
|
||||
ProfileImage(
|
||||
size = alertProfileImageSize,
|
||||
image = contact.profile.image,
|
||||
icon = contact.chatIconName
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmText = generalGetString(if (contact.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat),
|
||||
onConfirm = {
|
||||
@@ -624,14 +626,16 @@ fun showPrepareContactAlert(
|
||||
profileName = contactShortLinkData.profile.displayName,
|
||||
profileFullName = contactShortLinkData.profile.fullName,
|
||||
profileImage = {
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
},
|
||||
information = ownerVerificationMessage(ownerVerification),
|
||||
confirmText = generalGetString(MR.strings.connect_plan_open_new_chat),
|
||||
|
||||
@@ -1942,7 +1942,8 @@ processChatCommand cxt nm = \case
|
||||
-- [incognito] generate profile for connection
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
let userData = contactShortLinkData (userProfileDirect user incognitoProfile Nothing True) Nothing
|
||||
linkProfile <- presentUserBadge' incognitoProfile user $ userProfileDirect user incognitoProfile Nothing True
|
||||
let userData = contactShortLinkData linkProfile Nothing
|
||||
userLinkData = UserInvLinkData userData
|
||||
-- TODO [certs rcv]
|
||||
(connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userLinkData) Nothing IKPQOn subMode
|
||||
@@ -1964,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 $ userProfileDirect user Nothing Nothing True
|
||||
sLnk <- updatePCCShortLinkData conn =<< presentUserBadge user (userProfileDirect user Nothing Nothing True)
|
||||
conn' <- withFastStore' $ \db -> do
|
||||
deletePCCIncognitoProfile db user pId
|
||||
updatePCCIncognito db user conn Nothing sLnk
|
||||
@@ -1983,9 +1984,10 @@ processChatCommand cxt nm = \case
|
||||
recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
let short = isJust $ connShortLink' =<< connLinkInv
|
||||
userLinkData_
|
||||
| short = Just $ UserInvLinkData $ contactShortLinkData (userProfileDirect newUser Nothing Nothing True) Nothing
|
||||
| otherwise = Nothing
|
||||
userLinkData_ <-
|
||||
if short
|
||||
then Just . UserInvLinkData . (`contactShortLinkData` Nothing) <$> presentUserBadge newUser (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
|
||||
ccLink' <- shortenCreatedLink ccLink
|
||||
@@ -2260,10 +2262,11 @@ processChatCommand cxt nm = \case
|
||||
Right _ -> throwError $ ChatErrorStore SEDuplicateContactLink
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
-- TODO [relays] relay: add identity, key to link data?
|
||||
let userData
|
||||
| isTrue userChatRelay = relayShortLinkData (userProfileDirect user Nothing Nothing True)
|
||||
| otherwise = contactShortLinkData (userProfileDirect user Nothing Nothing True) Nothing
|
||||
userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
|
||||
userData <-
|
||||
if isTrue userChatRelay
|
||||
then pure $ relayShortLinkData (userProfileDirect user Nothing Nothing True)
|
||||
else (`contactShortLinkData` Nothing) <$> presentUserBadge user (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
|
||||
ccLink' <- shortenCreatedLink ccLink
|
||||
@@ -3762,9 +3765,9 @@ 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
|
||||
let shortLinkProfile = userProfileDirect user Nothing Nothing True
|
||||
-- TODO [short links] do not save address to server if data did not change, spinners, error handling
|
||||
userData
|
||||
shortLinkProfile <- presentUserBadge user $ 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
|
||||
| otherwise = contactShortLinkData shortLinkProfile $ Just addressSettings
|
||||
userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
|
||||
@@ -4072,7 +4075,7 @@ processChatCommand cxt nm = \case
|
||||
Just r -> pure r
|
||||
Nothing -> do
|
||||
(FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l'
|
||||
contactSLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData)
|
||||
let ov = verifyLinkOwner rootKey [] l sig_
|
||||
invitationReqAndPlan cReq (Just l') contactSLinkData_ ov
|
||||
where
|
||||
@@ -4099,7 +4102,7 @@ processChatCommand cxt nm = \case
|
||||
withFastStore' (\db -> getContactWithoutConnViaShortAddress db cxt user l') >>= \case
|
||||
Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct'))
|
||||
_ -> do
|
||||
contactSLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData)
|
||||
let ContactLinkData _ UserContactData {owners} = cData
|
||||
ov = verifyLinkOwner rootKey owners l' sig_
|
||||
plan <- contactRequestPlan user cReq contactSLinkData_ ov
|
||||
@@ -4268,7 +4271,7 @@ processChatCommand cxt nm = \case
|
||||
contactShortLinkData p settings =
|
||||
let msg = autoReply =<< settings
|
||||
business = maybe False businessAddress settings
|
||||
contactData = ContactShortLinkData p msg business
|
||||
contactData = ContactShortLinkData p msg business Nothing
|
||||
in encodeShortLinkData contactData
|
||||
relayShortLinkData :: Profile -> UserLinkData
|
||||
relayShortLinkData Profile {displayName, fullName, shortDescr, image} =
|
||||
|
||||
@@ -53,7 +53,7 @@ import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time (addUTCTime)
|
||||
import Data.Time.Calendar (fromGregorian)
|
||||
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds, secondsToDiffTime)
|
||||
import Simplex.Chat.Badges (Badge (..), BadgePresHeader (..), LocalBadge (..), badgeProof)
|
||||
import Simplex.Chat.Badges (Badge (..), BadgePresHeader (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Files
|
||||
@@ -1913,6 +1913,17 @@ 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
|
||||
linkDataBadge cld@ContactShortLinkData {profile = Profile {badge}} = case badge of
|
||||
Nothing -> pure cld
|
||||
Just b@(BadgeProof _ _ info) -> do
|
||||
key <- asks $ badgePublicKey . config
|
||||
verified <- liftIO $ verifyBadge key b
|
||||
now <- liftIO getCurrentTime
|
||||
pure (cld :: ContactShortLinkData) {localBadge = Just $ ShownBadge info (mkBadgeStatus now verified info)}
|
||||
|
||||
sendDirectContactMessage :: MsgEncodingI e => User -> Contact -> ChatMsgEvent e -> CM (SndMessage, Int64)
|
||||
sendDirectContactMessage user ct chatMsgEvent = do
|
||||
conn@Connection {connId} <- liftEither $ contactSendConn_ ct
|
||||
|
||||
@@ -49,6 +49,7 @@ import Data.Time.Clock.System (systemToUTCTime, utcToSystemTime)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable (Typeable)
|
||||
import Data.Word (Word32)
|
||||
import Simplex.Chat.Badges (LocalBadge)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
|
||||
import Simplex.Chat.Types
|
||||
@@ -1483,7 +1484,10 @@ instance FromField (ChatMessage 'Json) where
|
||||
data ContactShortLinkData = ContactShortLinkData
|
||||
{ profile :: Profile,
|
||||
message :: Maybe MsgContent,
|
||||
business :: Bool
|
||||
business :: Bool,
|
||||
-- set by the receiving client for the UI: the link profile's badge, verified and crypto-free.
|
||||
-- never part of the published link data (the link carries the proof inside profile).
|
||||
localBadge :: Maybe LocalBadge
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ chatProfileTests = do
|
||||
it "supporter badge sent to member joining via group link" testUserBadgeGroupLink
|
||||
it "expired supporter badge shows as expired" testUserBadgeExpired
|
||||
it "incognito connection does not carry supporter badge" testUserBadgeIncognito
|
||||
it "supporter badge sent to contact connecting via address" testUserBadgeContactAddress
|
||||
describe "user contact link" $ do
|
||||
it "create and connect via contact link" testUserContactLink
|
||||
it "retry connecting via contact link" testRetryConnectingViaContactLink
|
||||
@@ -290,6 +291,40 @@ testUserBadgeGroupLink ps = do
|
||||
bob <## "connection not verified, use /code command to see security code"
|
||||
bob <## currentChatVRangeInfo
|
||||
|
||||
testUserBadgeContactAddress :: HasCallStack => TestParams -> IO ()
|
||||
testUserBadgeContactAddress ps = do
|
||||
Right (sk, pk) <- bbsKeyGen
|
||||
testChatCfg2 (testCfg {badgePublicKey = pk}) aliceProfile bobProfile (test sk pk) ps
|
||||
where
|
||||
test sk pk alice bob = do
|
||||
addTestBadge alice =<< issueTestBadge sk pk Nothing
|
||||
alice ##> "/ad"
|
||||
(shortLink, cLink) <- getContactLinks alice True
|
||||
-- the address link data carries the badge proof; the connect plan returns it verified, without crypto
|
||||
bob ##> ("/_connect plan 1 " <> shortLink)
|
||||
bob <## "contact address: ok to connect"
|
||||
sLinkData <- getTermLine bob
|
||||
sLinkData `shouldContain` "\"proof\":"
|
||||
sLinkData `shouldContain` "\"localBadge\":{\"badge\":{\"badgeType\":\"supporter\""
|
||||
sLinkData `shouldContain` "\"status\":\"active\""
|
||||
bob ##> ("/c " <> cLink)
|
||||
alice <#? bob
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
|
||||
concurrently_
|
||||
(bob <## "alice (Alice, * supporter): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
bob ##> "/i alice"
|
||||
bob <## "contact ID: 2"
|
||||
bob <## "supporter badge - active"
|
||||
bob <## "no expiry"
|
||||
bob <## "receiving messages via: localhost"
|
||||
bob <## "sending messages via: localhost"
|
||||
bob <## "you've shared main profile with this contact"
|
||||
bob <## "connection not verified, use /code command to see security code"
|
||||
bob <## "quantum resistant end-to-end encryption"
|
||||
bob <## currentChatVRangeInfo
|
||||
|
||||
testUserBadgeExpired :: HasCallStack => TestParams -> IO ()
|
||||
testUserBadgeExpired ps = do
|
||||
Right (sk, pk) <- bbsKeyGen
|
||||
|
||||
Reference in New Issue
Block a user