diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 75a5baafee..2f9e328469 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -16,6 +16,7 @@ struct CIFileView: View { @EnvironmentObject var theme: AppTheme let file: CIFile? let edited: Bool + let msgVerified: MsgVerified let senderProfile: LocalProfile? var smallViewSize: CGFloat? @@ -24,9 +25,9 @@ struct CIFileView: View { fileIndicator() .simultaneousGesture(TapGesture().onEnded(fileAction)) } else { - let metaReserve = edited - ? " " - : " " + // the signature/"signature missing" icon in the overlaid meta needs reserved space too (matches CIMetaView) + let signedReserve = (msgVerified.verified && (file?.loaded ?? true)) || msgVerified == .sigMissing ? " " : "" + let metaReserve = (edited ? " " : " ") + signedReserve HStack(alignment: .bottom, spacing: 6) { fileIndicator() .padding(.top, 5) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index 69c6e0cd91..043bf53853 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -151,9 +151,11 @@ func ciMetaText( space = textSpace } if meta.msgVerified.verified && signedFileVerified { + appendSpace() r = r + colored(Text(Image("signature.plain")), resolved) space = textSpace } else if meta.msgVerified == .sigMissing { + appendSpace() r = r + colored(Text(Image(systemName: "exclamationmark.triangle")), colorMode.resolve(.red)) space = textSpace } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index fa16675610..42897af835 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -349,7 +349,7 @@ struct FramedItemView: View { } @ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View { - CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited, senderProfile: ciSenderProfile(chatItem, chat.chatInfo)) + CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited, msgVerified: chatItem.meta.msgVerified, senderProfile: ciSenderProfile(chatItem, chat.chatInfo)) .overlay(DetermineWidth()) if text != "" || ci.meta.isLive { ciMsgContentView (chatItem) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 292d6e1e42..968192ef16 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -120,7 +120,7 @@ struct GroupPreferencesView: View { get: { enableFeature.wrappedValue == .on }, set: { on, _ in enableFeature.wrappedValue = on ? .on : .off } ) - settingsRow(icon, color: color) { + settingsRow(icon, color: color, customImage: feature.customImage) { Toggle(feature.text(isChannel: groupInfo.isChannel), isOn: enable) } .disabled(disabled) @@ -143,7 +143,7 @@ struct GroupPreferencesView: View { .frame(height: 36) } } else { - settingsRow(icon, color: color) { + settingsRow(icon, color: color, customImage: feature.customImage) { infoRow(Text(feature.text(isChannel: groupInfo.isChannel)), enableFeature.wrappedValue.text) } if timedOn { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index a6e7fc5870..acb3662c00 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -438,7 +438,7 @@ struct ChatPreviewView: View { } case .file: smallContentPreviewFile(size: dynamicMediaSize) { - CIFileView(file: ci.file, edited: ci.meta.itemEdited, senderProfile: ciSenderProfile(ci, chat.chatInfo), smallViewSize: dynamicMediaSize) + CIFileView(file: ci.file, edited: ci.meta.itemEdited, msgVerified: ci.meta.msgVerified, senderProfile: ciSenderProfile(ci, chat.chatInfo), smallViewSize: dynamicMediaSize) } case let .chat(_, chatLink, ownerSig): smallContentPreview(size: dynamicMediaSize, borderColor: chatLink.image != nil ? .secondary : .clear) { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 2cb5698f8b..55e17c1e62 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -530,9 +530,11 @@ struct SettingsView: View { } } -func settingsRow(_ icon: String, color: Color/* = .secondary*/, content: @escaping () -> Content) -> some View { +func settingsRow(_ icon: String, color: Color/* = .secondary*/, customImage: Bool = false, content: @escaping () -> Content) -> some View { ZStack(alignment: .leading) { - Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center) + // custom symbol sets (e.g. "signature.plain") are asset images, SF Symbols are system images + (customImage ? Image(icon) : Image(systemName: icon)) + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) .symbolRenderingMode(.monochrome) .foregroundColor(color) content().padding(.leading, indent) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 4311d999d7..74fb067409 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1005,7 +1005,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .reports: return "flag" case .history: return "clock" case .support: return "questionmark.circle" - case .signMessages: return "signature" + case .signMessages: return "signature.plain" } } @@ -1021,7 +1021,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .reports: return "flag.fill" case .history: return "clock.fill" case .support: return "questionmark.circle.fill" - case .signMessages: return "signature" + case .signMessages: return "signature.plain" } } @@ -1032,6 +1032,13 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { } } + public var customImage: Bool { + switch self { + case .signMessages: return true + default: return false + } + } + public func enableDescription(_ enabled: GroupFeatureEnabled, _ canEdit: Bool, isChannel: Bool = false) -> LocalizedStringKey { if canEdit { switch self { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index f081ede15c..b8c5aab56c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt @@ -110,8 +110,10 @@ private fun CIMetaText( StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color) } if (meta.msgVerified.verified && signedFileVerified) { + Spacer(Modifier.width(4.dp)) StatusIconText(painterResource(MR.images.ic_signature), color) } else if (meta.msgVerified is MsgVerified.SigMissing) { + Spacer(Modifier.width(4.dp)) StatusIconText(painterResource(MR.images.ic_warning), Color.Red) } @@ -176,6 +178,7 @@ fun reserveSpaceForMeta( space = whiteSpace } if ((meta.msgVerified.verified && signedFileVerified) || meta.msgVerified is MsgVerified.SigMissing) { + appendSpace() res += iconSpace space = whiteSpace } diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index ce5c834001..b9c13ad279 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -139,6 +139,7 @@ This file is generated automatically. - [MsgReaction](#msgreaction) - [MsgReceiptStatus](#msgreceiptstatus) - [MsgSigStatus](#msgsigstatus) +- [MsgVerified](#msgverified) - [NameErrorType](#nameerrortype) - [NetworkError](#networkerror) - [NewUser](#newuser) @@ -872,7 +873,7 @@ Group: - editable: bool - forwardedByMember: int64? - showGroupAsSender: bool -- msgSigned: [MsgSigStatus](#msgsigstatus)? +- msgVerified: [MsgVerified](#msgverified) - createdAt: UTCTime - updatedAt: UTCTime @@ -2218,6 +2219,7 @@ Phone: - support: [SupportGroupPreference](#supportgrouppreference) - sessions: [RoleGroupPreference](#rolegrouppreference) - comments: [CommentsGroupPreference](#commentsgrouppreference) +- signMessages: [GroupPreference](#grouppreference) - commands: [[ChatBotCommand](#chatbotcommand)] @@ -2310,6 +2312,7 @@ MemberSupport: - "support" - "sessions" - "comments" +- "signMessages" --- @@ -2551,6 +2554,7 @@ UpdateRequired: - support: [SupportGroupPreference](#supportgrouppreference)? - sessions: [RoleGroupPreference](#rolegrouppreference)? - comments: [CommentsGroupPreference](#commentsgrouppreference)? +- signMessages: [GroupPreference](#grouppreference)? - commands: [[ChatBotCommand](#chatbotcommand)]? @@ -2951,6 +2955,23 @@ Unknown: - "signedNoKey" +--- + +## MsgVerified + +**Discriminated union type**: + +Signed: +- type: "signed" +- sigStatus: [MsgSigStatus](#msgsigstatus) + +SigMissing: +- type: "sigMissing" + +Unsigned: +- type: "unsigned" + + --- ## NameErrorType diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 35ea9fe261..5e1e2bb082 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -322,6 +322,7 @@ chatTypesDocsData = (sti @MsgReaction, STUnion, "MR", [], "", ""), (sti @MsgReceiptStatus, STEnum, "MR", [], "", ""), (sti @MsgSigStatus, STEnum, "MSS", [], "", ""), + (sti @MsgVerified, STUnion, "MV", [], "", ""), (sti @NameErrorType, STUnion, "", [], "", ""), (sti @NetworkError, STUnion, "NE", [], "", ""), (sti @NewUser, STRecord, "", [], "", ""), @@ -554,6 +555,7 @@ deriving instance Generic MsgFilter deriving instance Generic MsgReaction deriving instance Generic MsgReceiptStatus deriving instance Generic MsgSigStatus +deriving instance Generic MsgVerified deriving instance Generic NameErrorType deriving instance Generic NetworkError deriving instance Generic NewUser diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index f949a2f92f..e225659c4d 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -240,7 +240,8 @@ toTypeInfo tr = [ "FullDeleteGroupPreference", "ReactionsGroupPreference", "ReportsGroupPreference", - "HistoryGroupPreference" + "HistoryGroupPreference", + "SignMessagesGroupPreference" ] roleGroupPrefTypes = [ "DirectMessagesGroupPreference", diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index dcd9a5581f..22c7667597 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -841,7 +841,7 @@ export interface CIMeta { editable: boolean forwardedByMember?: number // int64 showGroupAsSender: boolean - msgSigned?: MsgSigStatus + msgVerified: MsgVerified createdAt: string // ISO-8601 timestamp updatedAt: string // ISO-8601 timestamp } @@ -2529,6 +2529,7 @@ export interface FullGroupPreferences { support: SupportGroupPreference sessions: RoleGroupPreference comments: CommentsGroupPreference + signMessages: GroupPreference commands: ChatBotCommand[] } @@ -2603,6 +2604,7 @@ export enum GroupFeature { Support = "support", Sessions = "sessions", Comments = "comments", + SignMessages = "signMessages", } export enum GroupFeatureEnabled { @@ -2812,6 +2814,7 @@ export interface GroupPreferences { support?: SupportGroupPreference sessions?: RoleGroupPreference comments?: CommentsGroupPreference + signMessages?: GroupPreference commands?: ChatBotCommand[] } @@ -3207,6 +3210,29 @@ export enum MsgSigStatus { SignedNoKey = "signedNoKey", } +export type MsgVerified = MsgVerified.Signed | MsgVerified.SigMissing | MsgVerified.Unsigned + +export namespace MsgVerified { + export type Tag = "signed" | "sigMissing" | "unsigned" + + interface Interface { + type: Tag + } + + export interface Signed extends Interface { + type: "signed" + sigStatus: MsgSigStatus + } + + export interface SigMissing extends Interface { + type: "sigMissing" + } + + export interface Unsigned extends Interface { + type: "unsigned" + } +} + export type NameErrorType = NameErrorType.NO_RESOLVER | NameErrorType.NOT_FOUND | NameErrorType.RESOLVER export namespace NameErrorType { diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 0856aaf772..4ebe75a8fd 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -587,7 +587,7 @@ class CIMeta(TypedDict): editable: bool forwardedByMember: NotRequired[int] # int64 showGroupAsSender: bool - msgSigned: NotRequired["MsgSigStatus"] + msgVerified: "MsgVerified" createdAt: str # ISO-8601 timestamp updatedAt: str # ISO-8601 timestamp @@ -1775,6 +1775,7 @@ class FullGroupPreferences(TypedDict): support: "SupportGroupPreference" sessions: "RoleGroupPreference" comments: "CommentsGroupPreference" + signMessages: "GroupPreference" commands: list["ChatBotCommand"] class FullPreferences(TypedDict): @@ -1818,7 +1819,7 @@ class GroupDirectInvitation(TypedDict): fromGroupMemberConnId_: NotRequired[int] # int64 groupDirectInvStartedConnection: bool -GroupFeature = Literal["timedMessages", "directMessages", "fullDelete", "reactions", "voice", "files", "simplexLinks", "reports", "history", "support", "sessions", "comments"] +GroupFeature = Literal["timedMessages", "directMessages", "fullDelete", "reactions", "voice", "files", "simplexLinks", "reports", "history", "support", "sessions", "comments", "signMessages"] GroupFeatureEnabled = Literal["on", "off"] @@ -1966,6 +1967,7 @@ class GroupPreferences(TypedDict): support: NotRequired["SupportGroupPreference"] sessions: NotRequired["RoleGroupPreference"] comments: NotRequired["CommentsGroupPreference"] + signMessages: NotRequired["GroupPreference"] commands: NotRequired[list["ChatBotCommand"]] class GroupProfile(TypedDict): @@ -2243,6 +2245,20 @@ MsgReceiptStatus = Literal["ok", "badMsgHash"] MsgSigStatus = Literal["verified", "signedNoKey"] +class MsgVerified_signed(TypedDict): + type: Literal["signed"] + sigStatus: "MsgSigStatus" + +class MsgVerified_sigMissing(TypedDict): + type: Literal["sigMissing"] + +class MsgVerified_unsigned(TypedDict): + type: Literal["unsigned"] + +MsgVerified = MsgVerified_signed | MsgVerified_sigMissing | MsgVerified_unsigned + +MsgVerified_Tag = Literal["signed", "sigMissing", "unsigned"] + class NameErrorType_NO_RESOLVER(TypedDict): type: Literal["NO_RESOLVER"]