core, ui: item about no e2ee in public channels (#6886)

* core, ui: item about no e2ee in public channels

* fix, refactor

* all tests

* update bot api types

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-04-25 20:49:26 +01:00
committed by GitHub
parent 67b2aff187
commit 504ef253cb
12 changed files with 63 additions and 35 deletions
@@ -172,8 +172,8 @@ struct ChatItemContentView<Content: View>: View {
case .rcvBlocked: deletedItemView()
case let .sndDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
case let .rcvDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
case .sndGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText())
case .rcvGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText())
case let .sndGroupE2EEInfo(e2eeInfo): CIEventView(eventText: groupE2EEInfoText(e2eeInfo))
case let .rcvGroupE2EEInfo(e2eeInfo): CIEventView(eventText: groupE2EEInfoText(e2eeInfo))
case .chatBanner: EmptyView()
case let .invalidJSON(json): CIInvalidJSONView(json: json)
}
@@ -257,6 +257,12 @@ struct ChatItemContentView<Content: View>: View {
e2eeInfoText("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.")
}
private func groupE2EEInfoText(_ info: E2EEInfo) -> Text {
info.public == true
? e2eeInfoText("Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages.")
: e2eeInfoNoPQText()
}
private func e2eeInfoText(_ s: LocalizedStringKey) -> Text {
Text(s)
.font(.caption)
+9 -2
View File
@@ -4092,8 +4092,8 @@ public enum CIContent: Decodable, ItemContent, Hashable {
case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item")
case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo)
case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo)
case .sndGroupE2EEInfo: return e2eeInfoNoPQStr
case .rcvGroupE2EEInfo: return e2eeInfoNoPQStr
case let .sndGroupE2EEInfo(e2eeInfo): return groupE2EEInfoStr(e2eeInfo)
case let .rcvGroupE2EEInfo(e2eeInfo): return groupE2EEInfoStr(e2eeInfo)
case .chatBanner: return ""
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
}
@@ -4105,6 +4105,12 @@ public enum CIContent: Decodable, ItemContent, Hashable {
: e2eeInfoNoPQStr
}
private func groupE2EEInfoStr(_ e2eeInfo: E2EEInfo) -> String {
e2eeInfo.public == true
? NSLocalizedString("Messages in this channel are not end-to-end encrypted. Chat relays can see these messages.", comment: "E2EE info chat item")
: e2eeInfoNoPQStr
}
private var e2eeInfoNoPQStr: String {
NSLocalizedString("This chat is protected by end-to-end encryption.", comment: "E2EE info chat item")
}
@@ -5319,6 +5325,7 @@ public enum CIGroupInvitationStatus: String, Decodable, Hashable {
public struct E2EEInfo: Decodable, Hashable {
public var pqEnabled: Bool?
public var `public`: Bool?
}
public enum RcvDirectEvent: Decodable, Hashable {
@@ -3800,8 +3800,8 @@ sealed class CIContent: ItemContent {
is RcvBlocked -> generalGetString(MR.strings.blocked_by_admin_item_description)
is SndDirectE2EEInfo -> directE2EEInfoStr(e2eeInfo)
is RcvDirectE2EEInfo -> directE2EEInfoStr(e2eeInfo)
is SndGroupE2EEInfo -> e2eeInfoNoPQStr
is RcvGroupE2EEInfo -> e2eeInfoNoPQStr
is SndGroupE2EEInfo -> groupE2EEInfoStr(e2eeInfo)
is RcvGroupE2EEInfo -> groupE2EEInfoStr(e2eeInfo)
is ChatBanner -> ""
is InvalidJSON -> "invalid data"
}
@@ -3838,6 +3838,9 @@ sealed class CIContent: ItemContent {
private val e2eeInfoNoPQStr: String = generalGetString(MR.strings.e2ee_info_no_pq_short)
fun groupE2EEInfoStr(e2EEInfo: E2EEInfo): String =
if (e2EEInfo.public == true) generalGetString(MR.strings.e2ee_info_no_e2ee) else e2eeInfoNoPQStr
fun featureText(feature: Feature, enabled: String, param: Int?, role: GroupMemberRole? = null): String =
(if (feature.hasParam) {
"${feature.text}: ${timeText(param)}"
@@ -4364,7 +4367,7 @@ enum class CIGroupInvitationStatus {
}
@Serializable
class E2EEInfo (val pqEnabled: Boolean?) {}
class E2EEInfo (val pqEnabled: Boolean?, val public: Boolean? = null) {}
object MsgContentSerializer : KSerializer<MsgContent> {
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) {
@@ -680,21 +680,17 @@ fun ChatItemView(
}
@Composable
fun E2EEInfoNoPQText() {
e2eeInfoText(MR.strings.e2ee_info_no_pq)
fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) {
e2eeInfoText(when (e2EEInfo.pqEnabled) {
true -> MR.strings.e2ee_info_pq
false -> MR.strings.e2ee_info_no_pq
null -> MR.strings.e2ee_info_e2ee
})
}
@Composable
fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) {
if (e2EEInfo.pqEnabled != null) {
if (e2EEInfo.pqEnabled) {
e2eeInfoText(MR.strings.e2ee_info_pq)
} else {
E2EEInfoNoPQText()
}
} else {
e2eeInfoText(MR.strings.e2ee_info_e2ee)
}
fun GroupE2EEInfoText(e2EEInfo: E2EEInfo) {
e2eeInfoText(if (e2EEInfo.public == true) MR.strings.e2ee_info_no_e2ee else MR.strings.e2ee_info_no_pq)
}
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
@@ -794,8 +790,8 @@ fun ChatItemView(
is CIContent.RcvBlocked -> DeletedItem()
is CIContent.SndDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
is CIContent.RcvDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
is CIContent.SndGroupE2EEInfo -> E2EEInfoNoPQText()
is CIContent.RcvGroupE2EEInfo -> E2EEInfoNoPQText()
is CIContent.SndGroupE2EEInfo -> GroupE2EEInfoText(c.e2eeInfo)
is CIContent.RcvGroupE2EEInfo -> GroupE2EEInfoText(c.e2eeInfo)
is CIContent.ChatBanner -> Spacer(modifier = Modifier.size(0.dp))
is CIContent.InvalidJSON -> {
CIInvalidJSONView(c.json)
@@ -74,6 +74,7 @@
<string name="e2ee_info_e2ee"><![CDATA[Messages are protected by <b>end-to-end encryption</b>.]]></string>
<string name="e2ee_info_no_pq"><![CDATA[Messages, files and calls are protected by <b>end-to-end encryption</b> with perfect forward secrecy, repudiation and break-in recovery.]]></string>
<string name="e2ee_info_pq"><![CDATA[Messages, files and calls are protected by <b>quantum resistant e2e encryption</b> with perfect forward secrecy, repudiation and break-in recovery.]]></string>
<string name="e2ee_info_no_e2ee"><![CDATA[Messages in this channel are <b>not end-to-end encrypted</b>. Chat relays can see these messages.]]></string>
<string name="e2ee_info_no_pq_short">This chat is protected by end-to-end encryption.</string>
<string name="e2ee_info_pq_short">This chat is protected by quantum resistant end-to-end encryption.</string>
+1
View File
@@ -1847,6 +1847,7 @@ connFullLink + ((' ' + connShortLink) if connShortLink is not None else '') # Py
## E2EInfo
**Record type**:
- public: bool?
- pqEnabled: bool?
@@ -2094,6 +2094,7 @@ export interface DroppedMsg {
}
export interface E2EInfo {
public?: boolean
pqEnabled?: boolean
}
+2 -2
View File
@@ -2008,7 +2008,7 @@ processChatCommand vr nm = \case
let cd = CDDirectRcv ct
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing
cInfo = DirectChat ct
void $ createItem Nothing $ CIRcvDirectE2EEInfo $ E2EInfo $ connRequestPQEncryption cReq
void $ createItem Nothing $ CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ connRequestPQEncryption cReq
void $ createFeatureEnabledItems_ user ct
aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent) message
let chat = case aci of
@@ -3858,7 +3858,7 @@ processChatCommand vr nm = \case
createNewGroupItems user gInfo = do
let cd = CDGroupSnd gInfo Nothing
createInternalChatItem user cd CIChatBanner (Just epochStart)
createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing
createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing
createGroupFeatureItems user cd CISndGroupFeature gInfo
sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM ()
sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
+3 -3
View File
@@ -1029,7 +1029,7 @@ acceptBusinessJoinRequestAsync
createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode
let cd = CDGroupSnd gInfo Nothing
-- TODO [short links] move to profileContactRequest?
createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing
createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing
createGroupFeatureItems user cd CISndGroupFeature gInfo
-- TODO [short links] get updated business chat group and member? (currently not used)
pure (gInfo, clientMember)
@@ -1485,7 +1485,7 @@ createContactPQSndItem :: User -> Contact -> Connection -> PQEncryption -> CM (C
createContactPQSndItem user ct conn@Connection {pqSndEnabled} pqSndEnabled' =
flip catchAllErrors (const $ pure (ct, conn)) $ case (pqSndEnabled, pqSndEnabled') of
(Just b, b') | b' /= b -> createPQItem $ CISndConnEvent (SCEPqEnabled pqSndEnabled')
(Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (E2EInfo $ Just pqSndEnabled')
(Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (e2eInfoEncrypted $ Just pqSndEnabled')
_ -> pure (ct, conn)
where
createPQItem ciContent = do
@@ -1500,7 +1500,7 @@ updateContactPQRcv :: User -> Contact -> Connection -> PQEncryption -> CM (Conta
updateContactPQRcv user ct conn@Connection {connId, pqRcvEnabled} pqRcvEnabled' =
flip catchAllErrors (const $ pure (ct, conn)) $ case (pqRcvEnabled, pqRcvEnabled') of
(Just b, b') | b' /= b -> updatePQ $ CIRcvConnEvent (RCEPqEnabled pqRcvEnabled')
(Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (E2EInfo $ Just pqRcvEnabled')
(Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (e2eInfoEncrypted $ Just pqRcvEnabled')
_ -> pure (ct, conn)
where
updatePQ ciContent = do
+4 -4
View File
@@ -590,7 +590,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- [incognito] print incognito profile used for this contact
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile)
let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo $ Just pqEnc) Nothing
let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ Just pqEnc) Nothing
-- TODO [short links] get contact request by contactRequestId, check encryption (UserContactRequest.pqSupport)?
when (directOrUsed ct') $ case (preparedContact ct', contactRequestId' ct') of
(Nothing, Nothing) -> do
@@ -842,7 +842,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
firstConnectedHost
( do
let cd = CDGroupRcv gInfo'' scopeInfo m''
createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing
createInternalChatItem user cd (CIRcvGroupE2EEInfo $ e2eInfoGroup gInfo'') Nothing
let prepared = preparedGroup gInfo''
unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo''
memberConnectedChatItem gInfo'' scopeInfo m''
@@ -1356,7 +1356,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
upsertDirectRequestItem cd (requestMsg_, prevSharedMsgId_)
Nothing -> do
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart)
let e2eContent = CIRcvDirectE2EEInfo $ E2EInfo $ Just $ CR.pqSupportToEnc $ reqPQSup
let e2eContent = CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ Just $ CR.pqSupportToEnc $ reqPQSup
void $ createChatItem user cd False e2eContent Nothing Nothing
void $ createFeatureEnabledItems_ user ct
forM_ (autoReply addressSettings) $ \mc -> forM_ welcomeSharedMsgId $ \sharedMsgId ->
@@ -2546,7 +2546,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- create item in both scopes
let gInfo' = gInfo {membership = membership'}
cd = CDGroupRcv gInfo' Nothing m
createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing
createInternalChatItem user cd (CIRcvGroupE2EEInfo $ e2eInfoGroup gInfo') Nothing
let prepared = preparedGroup gInfo'
unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo'
let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> preparedGroup gInfo'
+16 -4
View File
@@ -177,9 +177,16 @@ data CIContent (d :: MsgDirection) where
deriving instance Show (CIContent d)
data E2EInfo = E2EInfo {pqEnabled :: Maybe PQEncryption}
-- stored in database, all changed must be backward compatible
data E2EInfo = E2EInfo {public :: Maybe Bool, pqEnabled :: Maybe PQEncryption}
deriving (Eq, Show)
e2eInfoEncrypted :: Maybe PQEncryption -> E2EInfo
e2eInfoEncrypted pqEnabled = E2EInfo {public = Nothing, pqEnabled}
e2eInfoGroup :: GroupInfo -> E2EInfo
e2eInfoGroup g = E2EInfo {public = if useRelays' g then Just True else Nothing, pqEnabled = Just PQEncOff}
ciMsgContent :: CIContent d -> Maybe MsgContent
ciMsgContent = \case
CISndMsgContent mc -> Just mc
@@ -315,9 +322,14 @@ directE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of
Nothing -> simpleE2EText
groupE2EInfoToText :: E2EInfo -> Text
groupE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of
Just _ -> e2eInfoNoPQText
Nothing -> simpleE2EText
groupE2EInfoToText E2EInfo {pqEnabled, public} = case public of
Just True -> publicGroupNoE2EText
_ -> case pqEnabled of
Just _ -> e2eInfoNoPQText
Nothing -> simpleE2EText
publicGroupNoE2EText :: Text
publicGroupNoE2EText = "This channel or group is NOT end-to-end encrypted."
simpleE2EText :: Text
simpleE2EText = "This conversation is protected by end-to-end encryption"
+2 -1
View File
@@ -28,6 +28,7 @@ import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHook
import Simplex.Chat.Library.Internal (uniqueMsgMentions, updatedMentionNames)
import Simplex.Chat.Markdown (parseMaybeMarkdownList)
import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId)
import Simplex.Chat.Messages.CIContent (publicGroupNoE2EText)
import Simplex.Chat.Options
import Simplex.Chat.Protocol (MsgMention (..), MsgContent (..), msgContentText)
import Simplex.Chat.Types
@@ -8857,7 +8858,7 @@ testChannelLinkAfterWelcomeUpdate ps =
shortLink' `shouldBe` shortLink
fullLink' `shouldBe` fullLink
memberJoinChannel "team" [bob] [alice] shortLink' fullLink' dan
dan #$> ("/_get chat #1 count=100", chat, groupFeaturesNoE2E <> [(0, "welcome to team"), (0, e2eeInfoNoPQStr), (0, "connected")])
dan #$> ("/_get chat #1 count=100", chat, groupFeaturesNoE2E <> [(0, "welcome to team"), (0, T.unpack publicGroupNoE2EText), (0, "connected")])
alice #> "#team hi"
bob <# "#team> hi"