diff --git a/apps/ios/Shared/SharedAssets.xcassets/Contents.json b/apps/ios/Shared/SharedAssets.xcassets/Contents.json new file mode 100644 index 0000000000..4aa7c5350b --- /dev/null +++ b/apps/ios/Shared/SharedAssets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/apps/ios/Shared/SharedAssets.xcassets/agent.imageset/Contents.json b/apps/ios/Shared/SharedAssets.xcassets/agent.imageset/Contents.json new file mode 100644 index 0000000000..300553af5f --- /dev/null +++ b/apps/ios/Shared/SharedAssets.xcassets/agent.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "agent.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/apps/ios/Shared/SharedAssets.xcassets/agent.imageset/agent.svg b/apps/ios/Shared/SharedAssets.xcassets/agent.imageset/agent.svg new file mode 100644 index 0000000000..fd0fdb485a --- /dev/null +++ b/apps/ios/Shared/SharedAssets.xcassets/agent.imageset/agent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift index aaaa29929d..1eebc5cf19 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift @@ -50,7 +50,7 @@ struct CIChatLinkHeader: View { HStack(alignment: .top) { ProfileImage( imageStr: chatLink.image, - iconName: chatLink.iconName, + icon: Image(systemName: chatLink.iconName), size: 44, color: Color(uiColor: .tertiaryLabel) ) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index fecd1ba891..efec44e943 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -115,7 +115,7 @@ struct CIGroupInvitationView: View { return HStack(alignment: .top) { ProfileImage( imageStr: groupInvitation.groupProfile.image, - iconName: "person.2.circle.fill", + icon: Image(systemName: "person.2.circle.fill"), size: 44, color: color ) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index d18d8ae145..29a1be3d65 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -1968,7 +1968,7 @@ struct ChatView: View { .padding(.trailing, 12) } HStack(alignment: .top, spacing: 10) { - ProfileImage(imageStr: groupInfo.image, iconName: groupInfo.chatIconName, size: memberImageSize, backgroundColor: theme.colors.background) + ProfileImage(imageStr: groupInfo.image, icon: Image(systemName: groupInfo.chatIconName), size: memberImageSize, backgroundColor: theme.colors.background) .simultaneousGesture(TapGesture().onEnded { showChatInfoSheet = true }) @@ -2052,7 +2052,7 @@ struct ChatView: View { } HStack(alignment: .top, spacing: 10) { if ci.meta.showGroupAsSender { - ProfileImage(imageStr: groupInfo.image, iconName: groupInfo.chatIconName, size: memberImageSize, backgroundColor: theme.colors.background) + ProfileImage(imageStr: groupInfo.image, icon: Image(systemName: groupInfo.chatIconName), size: memberImageSize, backgroundColor: theme.colors.background) .simultaneousGesture(TapGesture().onEnded { showChatInfoSheet = true }) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift index 650ea8a87f..78f29c3b60 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift @@ -11,7 +11,7 @@ struct ComposeChatLinkView: View { HStack(alignment: .center, spacing: 8) { ProfileImage( imageStr: chatLink.image, - iconName: chatLink.iconName, + icon: Image(systemName: chatLink.iconName), size: 44 ) .padding(.leading, 12) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 126fcc57b3..19478753af 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -37,7 +37,7 @@ struct GroupProfileView: View { var body: some View { List { - EditProfileImage(profileImage: $groupProfile.image, iconName: groupInfo.chatIconName, showChooseSource: $showChooseSource) + EditProfileImage(profileImage: $groupProfile.image, icon: Image(systemName: groupInfo.chatIconName), showChooseSource: $showChooseSource) .if(!focusDisplayName) { $0.padding(.top) } Section { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 5ee991a466..8156ea81c7 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -444,7 +444,7 @@ struct ChatPreviewView: View { smallContentPreview(size: dynamicMediaSize, borderColor: chatLink.image != nil ? .secondary : .clear) { ProfileImage( imageStr: chatLink.image, - iconName: chatLink.iconName, + icon: Image(systemName: chatLink.iconName), size: dynamicMediaSize, color: Color(uiColor: .tertiaryLabel) ) diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift index 40d62e009b..a0f21aef9b 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -19,7 +19,7 @@ struct ChatInfoImage: View { let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color } return ProfileImage( imageStr: chat.chatInfo.image, - iconName: chatIconName(chat.chatInfo), + icon: chatIcon(chat.chatInfo), size: size, color: iconColor ) diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index 9c2916880c..05b4a06967 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -12,7 +12,7 @@ import SimpleXChat struct ProfileImage: View { @EnvironmentObject var theme: AppTheme var imageStr: String? = nil - var iconName: String = "person.crop.circle.fill" + var icon: Image = Image(systemName: "person.crop.circle.fill") var size: CGFloat var color = Color(uiColor: .tertiarySystemGroupedBackground) var backgroundColor: Color? = nil @@ -24,7 +24,7 @@ struct ProfileImage: View { clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred) } else { let c = color.asAnotherColorFromSecondaryVariant(theme) - Image(systemName: iconName) + icon .resizable() .foregroundColor(c) .scaledToFit() diff --git a/apps/ios/Shared/Views/NewChat/AddChannelView.swift b/apps/ios/Shared/Views/NewChat/AddChannelView.swift index 7d1e5ce827..fa953b2ec1 100644 --- a/apps/ios/Shared/Views/NewChat/AddChannelView.swift +++ b/apps/ios/Shared/Views/NewChat/AddChannelView.swift @@ -50,7 +50,7 @@ struct AddChannelView: View { Spacer(minLength: 0) ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, iconName: "antenna.radiowaves.left.and.right.circle.fill", size: 128) + ProfileImage(imageStr: profile.image, icon: Image(systemName: "antenna.radiowaves.left.and.right.circle.fill"), size: 128) if profile.image != nil { Button { profile.image = nil @@ -285,7 +285,7 @@ struct AddChannelView: View { let total = groupRelays.count return List { Group { - ProfileImage(imageStr: gInfo.groupProfile.image, iconName: "antenna.radiowaves.left.and.right.circle.fill", size: 128) + ProfileImage(imageStr: gInfo.groupProfile.image, icon: Image(systemName: "antenna.radiowaves.left.and.right.circle.fill"), size: 128) .frame(maxWidth: .infinity, alignment: .center) Text(gInfo.groupProfile.displayName) diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 47afee5f06..a4f02e51ba 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -71,7 +71,7 @@ struct AddGroupView: View { Spacer(minLength: 0) ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, iconName: "person.2.circle.fill", size: 128) + ProfileImage(imageStr: profile.image, icon: Image(systemName: "person.2.circle.fill"), size: 128) if profile.image != nil { Button { profile.image = nil diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index ef691b10f9..b558802d36 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -1129,11 +1129,9 @@ private func showPrepareContactAlert( profileImage: ProfileImage( imageStr: contactShortLinkData.profile.image, - iconName: contactShortLinkData.business - ? "briefcase.circle.fill" - : contactShortLinkData.profile.peerType == .bot - ? "cube.fill" - : "person.crop.circle.fill", + icon: contactShortLinkData.business + ? Image(systemName: "briefcase.circle.fill") + : peerTypeIcon(contactShortLinkData.profile.peerType), size: alertProfileImageSize ), profileBadge: contactShortLinkData.localBadge, @@ -1186,9 +1184,9 @@ private func showPrepareGroupAlert( profileImage: ProfileImage( imageStr: groupShortLinkData.groupProfile.image, - iconName: isChannel + icon: Image(systemName: isChannel ? "antenna.radiowaves.left.and.right.circle.fill" - : "person.2.circle.fill", + : "person.2.circle.fill"), size: alertProfileImageSize ), theme: theme, @@ -1240,7 +1238,7 @@ private func showOpenKnownContactAlert( profileImage: ProfileImage( imageStr: contact.profile.image, - iconName: contact.chatIconName, + icon: contact.chatIcon, size: alertProfileImageSize ), profileBadge: contact.active ? contact.profile.localBadge : nil, @@ -1288,7 +1286,7 @@ private func showOpenKnownGroupAlert( profileImage: ProfileImage( imageStr: groupInfo.groupProfile.image, - iconName: groupInfo.chatIconName, + icon: Image(systemName: groupInfo.chatIconName), size: alertProfileImageSize ), theme: theme, @@ -1605,7 +1603,7 @@ func planAndConnect( profileImage: ProfileImage( imageStr: groupSLinkData.groupProfile.image, - iconName: "antenna.radiowaves.left.and.right.circle.fill", + icon: Image(systemName: "antenna.radiowaves.left.and.right.circle.fill"), size: alertProfileImageSize ), theme: theme, @@ -1632,7 +1630,7 @@ func planAndConnect( profileImage: ProfileImage( imageStr: groupSLinkData.groupProfile.image, - iconName: "person.2.circle.fill", + icon: Image(systemName: "person.2.circle.fill"), size: alertProfileImageSize ), theme: theme, diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 8fe8a1c45a..64c877f9cd 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -29,7 +29,7 @@ struct UserProfile: View { var body: some View { List { - EditProfileImage(profileImage: $profile.image, iconName: "person.crop.circle.fill", showChooseSource: $showChooseSource) + EditProfileImage(profileImage: $profile.image, icon: Image(systemName: "person.crop.circle.fill"), showChooseSource: $showChooseSource) .padding(.top) Section { @@ -221,7 +221,7 @@ struct EditProfileImage: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner @Binding var profileImage: String? - var iconName: String + var icon: Image @Binding var showChooseSource: Bool var body: some View { @@ -237,7 +237,7 @@ struct EditProfileImage: View { } } else { ZStack(alignment: .center) { - ProfileImage(imageStr: profileImage, iconName: iconName, size: 160) + ProfileImage(imageStr: profileImage, icon: icon, size: 160) editImageButton { showChooseSource = true } } } diff --git a/apps/ios/SimpleX SE/ShareView.swift b/apps/ios/SimpleX SE/ShareView.swift index 3bdbc950ba..fd163f74e5 100644 --- a/apps/ios/SimpleX SE/ShareView.swift +++ b/apps/ios/SimpleX SE/ShareView.swift @@ -25,7 +25,7 @@ struct ShareView: View { HStack { profileImage( chatInfoId: chat.chatInfo.id, - iconName: chatIconName(chat.chatInfo), + icon: chatIcon(chat.chatInfo), size: 30 ) Text(chat.chatInfo.displayName).foregroundStyle( @@ -209,11 +209,11 @@ struct ShareView: View { .background(Material.ultraThin) } - @ViewBuilder private func profileImage(chatInfoId: ChatInfo.ID, iconName: String, size: Double) -> some View { + @ViewBuilder private func profileImage(chatInfoId: ChatInfo.ID, icon: Image, size: Double) -> some View { if let uiImage = model.profileImages[chatInfoId] { clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius) } else { - Image(systemName: iconName) + icon .resizable() .foregroundColor(Color(uiColor: .tertiaryLabel)) .frame(width: size, height: size) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index fb29f753b0..389017cce0 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -271,6 +271,8 @@ E5AEC0AF2F91A73500270665 /* ComposeChatLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AEC0AE2F91A73500270665 /* ComposeChatLinkView.swift */; }; E5C0BBE82F82B45500EA7527 /* SimpleXAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E5C0BBE72F82B45500EA7527 /* SimpleXAssets.xcassets */; }; E5C0BBE92F82B45500EA7527 /* SimpleXAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E5C0BBE72F82B45500EA7527 /* SimpleXAssets.xcassets */; }; + E5C0BBEB2F82B45500EA7527 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E5C0BBEA2F82B45500EA7527 /* SharedAssets.xcassets */; }; + E5C0BBEC2F82B45500EA7527 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E5C0BBEA2F82B45500EA7527 /* SharedAssets.xcassets */; }; E5DBF1932F88169800E1D7FD /* ConnectBannerCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DBF1922F88169800E1D7FD /* ConnectBannerCard.swift */; }; E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; @@ -662,6 +664,7 @@ E5AEC0AA2F91A6EA00270665 /* CIChatLinkHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatLinkHeader.swift; sourceTree = ""; }; E5AEC0AE2F91A73500270665 /* ComposeChatLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeChatLinkView.swift; sourceTree = ""; }; E5C0BBE72F82B45500EA7527 /* SimpleXAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = SimpleXAssets.xcassets; sourceTree = ""; }; + E5C0BBEA2F82B45500EA7527 /* SharedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = SharedAssets.xcassets; sourceTree = ""; }; E5C0BBFD2F82BBC000EA7527 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; E5C0BBFE2F82BBC900EA7527 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; E5DBF1922F88169800E1D7FD /* ConnectBannerCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectBannerCard.swift; sourceTree = ""; }; @@ -953,6 +956,7 @@ 5C2E260D27A30E2400F70299 /* Views */, 5CA059C5279559F40002BEB4 /* Assets.xcassets */, E5C0BBE72F82B45500EA7527 /* SimpleXAssets.xcassets */, + E5C0BBEA2F82B45500EA7527 /* SharedAssets.xcassets */, D7AA2C3429A936B400737B40 /* MediaEncryption.playground */, 5C13730C2815740A00F43030 /* DebugJSON.playground */, ); @@ -1506,6 +1510,7 @@ 5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */, 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */, E5C0BBE82F82B45500EA7527 /* SimpleXAssets.xcassets in Resources */, + E5C0BBEB2F82B45500EA7527 /* SharedAssets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1538,6 +1543,7 @@ files = ( E5DCF9712C590272007928CC /* Localizable.strings in Resources */, E5C0BBE92F82B45500EA7527 /* SimpleXAssets.xcassets in Resources */, + E5C0BBEC2F82B45500EA7527 /* SharedAssets.xcassets in Resources */, E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index a27f396a54..a4d65a6d10 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -228,6 +228,7 @@ public enum ChatPeerType: Hashable { case human case bot case business + case agent case unknown(String) public var text: String { @@ -235,6 +236,7 @@ public enum ChatPeerType: Hashable { case .human: "human" case .bot: "bot" case .business: "business" + case .agent: "agent" case let .unknown(s): s } } @@ -246,6 +248,7 @@ extension ChatPeerType: Codable { case "human": self = .human case "bot": self = .bot case "business": self = .business + case "agent": self = .agent case let s: self = .unknown(s) } } @@ -256,6 +259,14 @@ extension ChatPeerType: Codable { } } +public func peerTypeIcon(_ peerType: ChatPeerType?) -> Image { + switch peerType { + case .bot: Image(systemName: "cube.fill") + case .agent: Image("agent") + default: Image(systemName: "person.crop.circle.fill") + } +} + // Supporter badge. The credential/proof bytes stay core-side; the UI only sees the disclosed type + status. // Unknown types keep their string so a verified badge's real name can be shown, while the icon falls back to supporter. public enum BadgeType: Hashable { @@ -2532,8 +2543,8 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { activeConn?.customUserProfileId != nil } - public var chatIconName: String { - isBot ? "cube.fill" : "person.crop.circle.fill" + public var chatIcon: Image { + peerTypeIcon(profile.peerType) } public func allowsFeature(_ feature: ChatFeature) -> Bool { diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 45320c5b93..ab9cad9de6 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -7,6 +7,7 @@ // import Foundation +import SwiftUI public protocol ChatLike { var chatInfo: ChatInfo { get } @@ -91,13 +92,13 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { } } -public func chatIconName(_ cInfo: ChatInfo) -> String { +public func chatIcon(_ cInfo: ChatInfo) -> Image { switch cInfo { - case let .direct(contact): contact.chatIconName - case let .group(groupInfo, _): groupInfo.chatIconName - case .local: "folder.circle.fill" - case .contactRequest: "person.crop.circle.fill" - default: "circle.fill" + case let .direct(contact): contact.chatIcon + case let .group(groupInfo, _): Image(systemName: groupInfo.chatIconName) + case .local: Image(systemName: "folder.circle.fill") + case .contactRequest: Image(systemName: "person.crop.circle.fill") + default: Image(systemName: "circle.fill") } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 3db735ecea..bf856c13af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1954,7 +1954,7 @@ data class Contact( activeConn?.customUserProfileId != null val chatIconName: ImageResource get() = - if (isBot) MR.images.ic_cube else MR.images.ic_account_circle_filled + peerTypeIcon(profile.peerType) fun allowsFeature(feature: ChatFeature): Boolean = when (feature) { ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO @@ -2165,6 +2165,7 @@ sealed class ChatPeerType { @Serializable @SerialName("human") object Human: ChatPeerType() @Serializable @SerialName("bot") object Bot: ChatPeerType() @Serializable @SerialName("business") object Business: ChatPeerType() + @Serializable @SerialName("agent") object Agent: ChatPeerType() @Serializable @SerialName("unknown") data class Unknown(val type: String): ChatPeerType() val text: String @@ -2172,6 +2173,7 @@ sealed class ChatPeerType { is Human -> "human" is Bot -> "bot" is Business -> "business" + is Agent -> "agent" is Unknown -> type } } @@ -2183,11 +2185,19 @@ object ChatPeerTypeSerializer : KSerializer { "human" -> ChatPeerType.Human "bot" -> ChatPeerType.Bot "business" -> ChatPeerType.Business + "agent" -> ChatPeerType.Agent else -> ChatPeerType.Unknown(v) } override fun serialize(encoder: Encoder, value: ChatPeerType) = encoder.encodeString(value.text) } +fun peerTypeIcon(peerType: ChatPeerType?): ImageResource = + when (peerType) { + ChatPeerType.Bot -> MR.images.ic_cube + ChatPeerType.Agent -> MR.images.ic_agent + else -> MR.images.ic_account_circle_filled + } + // Supporter badge. The credential/proof bytes stay core-side; the UI only sees the disclosed type + status. // Unknown types keep their string so a verified badge's real name can be shown, while the icon falls back to supporter. @Serializable(with = BadgeTypeSerializer::class) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 785965eff4..54d684160e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -8139,7 +8139,7 @@ sealed class StoreError { is UserChatRelayNotFound -> "userChatRelayNotFound $chatRelayId" is GroupRelayNotFound -> "groupRelayNotFound $groupRelayId" is GroupRelayNotFoundByMemberId -> "groupRelayNotFoundByMemberId $groupMemberId" - is DuplicateGroupMessage -> "duplicateGroupMessage $groupId $sharedMsgId $authorGroupMemberId $authorGroupMemberId" + is DuplicateGroupMessage -> "duplicateGroupMessage $groupId $sharedMsgId $authorGroupMemberId $forwardedByGroupMemberId" is RemoteHostNotFound -> "remoteHostNotFound $remoteHostId" is RemoteHostUnknown -> "remoteHostUnknown" is RemoteHostDuplicateCA -> "remoteHostDuplicateCA" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 61cb73ca0b..8b145ef206 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -717,8 +717,7 @@ fun showPrepareContactAlert( 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 + else peerTypeIcon(contactShortLinkData.profile.peerType) ) }, profileBadge = if (contactShortLinkData.localBadge?.status == BadgeStatus.ExpiredOld) null else contactShortLinkData.localBadge, diff --git a/apps/multiplatform/common/src/commonMain/moko-resources/images/ic_agent.svg b/apps/multiplatform/common/src/commonMain/moko-resources/images/ic_agent.svg new file mode 100644 index 0000000000..e03226d798 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/moko-resources/images/ic_agent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index fe79d847e2..6ca0d67ac4 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -1545,6 +1545,7 @@ Search: - "human" - "bot" - "business" +- "agent" --- diff --git a/docs/protocol/simplex-chat.schema.json b/docs/protocol/simplex-chat.schema.json index 4e9c3c73a0..5fb4794094 100644 --- a/docs/protocol/simplex-chat.schema.json +++ b/docs/protocol/simplex-chat.schema.json @@ -25,7 +25,7 @@ "metadata": {"comment": "profile bio (160 characters)"} }, "contactLink": {"ref": "connReqUri"}, - "peerType": {"enum": ["human", "bot"]}, + "peerType": {"enum": ["human", "bot", "agent"]}, "preferences": { "optionalProperties": { "commands": { diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index cebc68abb4..35711c62cf 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -1796,6 +1796,7 @@ export enum ChatPeerType { Human = "human", Bot = "bot", Business = "business", + Agent = "agent", } // Used in API commands. Chat scope can only be passed with groups. 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 1befa750c3..f732d7faf2 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -1247,7 +1247,7 @@ ChatListQuery = ChatListQuery_filters | ChatListQuery_search ChatListQuery_Tag = Literal["filters", "search"] -ChatPeerType = Literal["human", "bot", "business"] +ChatPeerType = Literal["human", "bot", "business", "agent"] # Used in API commands. Chat scope can only be passed with groups. diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 81ed6ac5a8..94fd5d4fcd 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -749,7 +749,7 @@ data Profile = Profile } deriving (Eq, Show) -data ChatPeerType = CPTHuman | CPTBot | CPTBusiness | CPTUnknown Text +data ChatPeerType = CPTHuman | CPTBot | CPTBusiness | CPTAgent | CPTUnknown Text deriving (Eq, Show) instance FromJSON ChatPeerType where @@ -768,11 +768,13 @@ instance TextEncoding ChatPeerType where "human" -> CPTHuman "bot" -> CPTBot "business" -> CPTBusiness + "agent" -> CPTAgent tag -> CPTUnknown tag textEncode = \case CPTHuman -> "human" CPTBot -> "bot" CPTBusiness -> "business" + CPTAgent -> "agent" CPTUnknown tag -> tag profileFromName :: ContactName -> Profile diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 35f1ea5885..fb9e8317c3 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -332,6 +332,9 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do it "x.info with empty full name" $ "{\"v\":\"9\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} Nothing + it "x.info with agent peer type" $ + "{\"v\":\"9\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"peerType\":\"agent\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTAgent, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} Nothing it "x.contact with xContactId" $ "{\"v\":\"9\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile Nothing (Just $ XContactId "\1\2\3\4") Nothing Nothing