diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 864c1a6e9e..9bc5a4420b 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -9,7 +9,7 @@ import SwiftUI import SimpleXChat -func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View { +func infoRow(_ title: S, _ value: String) -> some View where S: StringProtocol { HStack { Text(title) Spacer() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift new file mode 100644 index 0000000000..c615497fc8 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -0,0 +1,41 @@ +// +// CIChatFeatureView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 21/11/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct CIChatFeatureView: View { + var chatItem: ChatItem + var feature: Feature + var enabled: FeatureEnabled? + + var body: some View { + HStack(alignment: .bottom, spacing: 0) { + Image(systemName: feature.icon + ".fill") + .foregroundColor(iconColor) + chatEventText(chatItem) + } + .padding(.leading, 6) + .padding(.bottom, 6) + .textSelection(.disabled) + } + + private var iconColor: Color { + if let enabled = enabled { + return enabled.forUser ? .green : enabled.forContact ? .yellow : .secondary + } + return .red + } +} + +struct CIChatFeatureView_Previews: PreviewProvider { + static var previews: some View { + let enabled = FeatureEnabled(forUser: false, forContact: false) + CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: .fullDelete, enabled: enabled) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift index 167186fde1..cd059b704c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift @@ -20,27 +20,27 @@ struct CIEventView: View { .foregroundColor(.secondary) .fontWeight(.light) + Text(" ") - + eventText() + + chatEventText(chatItem) } else { - eventText() + chatEventText(chatItem) } } .padding(.leading, 6) .padding(.bottom, 6) .textSelection(.disabled) } +} - func eventText() -> Text { - Text(chatItem.content.text) - .font(.caption) - .foregroundColor(.secondary) - .fontWeight(.light) - + Text(" ") - + chatItem.timestampText - .font(.caption) - .foregroundColor(Color.secondary) - .fontWeight(.light) - } +func chatEventText(_ ci: ChatItem) -> Text { + Text(ci.content.text) + .font(.caption) + .foregroundColor(.secondary) + .fontWeight(.light) + + Text(" ") + + ci.timestampText + .font(.caption) + .foregroundColor(Color.secondary) + .fontWeight(.light) } struct CIEventView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index df91529a6b..4b19dba173 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -31,6 +31,9 @@ struct ChatItemView: View { case .sndGroupEvent: eventItemView() case .rcvConnEvent: eventItemView() case .sndConnEvent: eventItemView() + case let .rcvChatFeature(feature, enabled): chatFeatureView(feature, enabled) + case let .sndChatFeature(feature, enabled): chatFeatureView(feature, enabled) + case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, nil) } } @@ -57,6 +60,10 @@ struct ChatItemView: View { private func eventItemView() -> some View { CIEventView(chatItem: chatItem) } + + private func chatFeatureView(_ feature: Feature, _ enabled: FeatureEnabled?) -> some View { + CIChatFeatureView(chatItem: chatItem, feature: feature, enabled: enabled) + } } struct ChatItemView_Previews: PreviewProvider { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 1566f60f6c..3cb11e241e 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */; }; 5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; }; 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; }; + 5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */; }; 5C58BCD0292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCB292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */; }; 5C58BCD1292AA93700AF9E4F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCC292AA93700AF9E4F /* libgmp.a */; }; 5C58BCD2292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */; }; @@ -247,6 +248,7 @@ 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; }; 5C55A92D283D0FDE00C4E99E /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = sounds; sourceTree = ""; }; 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = ""; }; + 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatFeatureView.swift; sourceTree = ""; }; 5C58BCCB292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a"; sourceTree = ""; }; 5C58BCCC292AA93700AF9E4F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a"; sourceTree = ""; }; @@ -672,6 +674,7 @@ 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */, 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */, 6440C9FF288857A10062C672 /* CIEventView.swift */, + 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */, ); path = ChatItem; sourceTree = ""; @@ -960,6 +963,7 @@ 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */, 5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */, 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */, + 5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */, 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */, 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 4eb77b9188..0ccbc81bc5 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -213,6 +213,13 @@ public struct FeatureEnabled: Decodable { default: return FeatureEnabled(forUser: true, forContact: true) } } + + public var text: String { + (forUser && forContact) ? NSLocalizedString("enabled", comment: "enabled status") + : forUser ? NSLocalizedString("enabled for you", comment: "enabled status") + : forContact ? NSLocalizedString("enabled for contact", comment: "enabled status") + : NSLocalizedString("off", comment: "enabled status") + } } public enum ContactUserPref: Decodable { @@ -220,7 +227,7 @@ public enum ContactUserPref: Decodable { case user(preference: Preference) // global user default is used } -public enum Feature { +public enum Feature: String, Decodable { case fullDelete case voice @@ -228,10 +235,10 @@ public enum Feature { public var id: Self { self } - public var text: LocalizedStringKey { + public var text: String { switch self { - case .fullDelete: return "Full deletion" - case .voice: return "Voice messages" + case .fullDelete: return NSLocalizedString("Full deletion", comment: "chat feature") + case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") } } @@ -1364,6 +1371,17 @@ public struct ChatItem: Identifiable, Decodable { file: nil ) } + + public static func getChatFeatureSample(_ feature: Feature, _ enabled: FeatureEnabled) -> ChatItem { + let content = CIContent.rcvChatFeature(feature: feature, enabled: enabled) + return ChatItem( + chatDir: .directRcv, + meta: CIMeta.getSample(1, .now, content.text, .rcvRead, false, false, false), + content: content, + quotedItem: nil, + file: nil + ) + } } public enum CIDirection: Decodable { @@ -1465,6 +1483,9 @@ public enum CIContent: Decodable, ItemContent { case sndGroupEvent(sndGroupEvent: SndGroupEvent) case rcvConnEvent(rcvConnEvent: RcvConnEvent) case sndConnEvent(sndConnEvent: SndConnEvent) + case rcvChatFeature(feature: Feature, enabled: FeatureEnabled) + case sndChatFeature(feature: Feature, enabled: FeatureEnabled) + case rcvChatFeatureRejected(feature: Feature) public var text: String { get { @@ -1482,6 +1503,9 @@ public enum CIContent: Decodable, ItemContent { case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text case let .sndConnEvent(sndConnEvent): return sndConnEvent.text + case let .rcvChatFeature(feature, enabled): return "\(feature.text): \(enabled.text)" + case let .sndChatFeature(feature, enabled): return "\(feature.text): \(enabled.text)" + case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text) } } } diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 1306f26eaa..a860b9c77e 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1750,6 +1750,7 @@ processAgentMessage (Just 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 $ CRContactConnected ct (fmap fromLocalProfile incognitoProfile) + createFeatureEnabledItems ct setActive $ ActiveC c showToast (c <> "> ") "connected" forM_ groupLinkId $ \_ -> probeMatchingContacts ct $ contactConnIncognito ct @@ -2483,6 +2484,23 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = xInfo c@Contact {profile = p} p' = unless (fromLocalProfile p == p') $ do c' <- withStore $ \db -> updateContactProfile db user c p' toView $ CRContactUpdated c c' + createFeatureChangedItems c' + where + createFeatureChangedItems c' = unless (preferences' c == preferences' c') $ do + let cups = contactUserPreferences' user c + cups' = contactUserPreferences' user c' + forM_ allChatFeatures $ \f -> do + let ContactUserPreference {enabled} = getContactUserPreference f cups + ContactUserPreference {enabled = enabled'} = getContactUserPreference f cups' + unless (enabled == enabled') $ + createInternalChatItem (CDDirectRcv c') (CIRcvChatFeature f enabled') Nothing + + createFeatureEnabledItems :: Contact -> m () + createFeatureEnabledItems ct = do + let cups = contactUserPreferences' user ct + forM_ allChatFeatures $ \f -> do + let ContactUserPreference {enabled} = getContactUserPreference f cups + createInternalChatItem (CDDirectRcv ct) (CIRcvChatFeature f enabled) Nothing xInfoProbe :: Contact -> Probe -> m () xInfoProbe c2 probe = diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 3c338a7f92..9aa28dfab5 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -558,6 +558,9 @@ data CIContent (d :: MsgDirection) where CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd CIRcvConnEvent :: RcvConnEvent -> CIContent 'MDRcv CISndConnEvent :: SndConnEvent -> CIContent 'MDSnd + CIRcvChatFeature :: ChatFeature -> PrefEnabled -> CIContent 'MDRcv + CISndChatFeature :: ChatFeature -> PrefEnabled -> CIContent 'MDSnd + CIRcvChatFeatureRejected :: ChatFeature -> CIContent 'MDRcv -- ^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API -- ! ^ Nested sum types also have to use different encodings for database and API -- ! ^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent @@ -709,6 +712,9 @@ ciContentToText = \case CISndGroupEvent event -> sndGroupEventToText event CIRcvConnEvent event -> rcvConnEventToText event CISndConnEvent event -> sndConnEventToText event + CIRcvChatFeature feature enabled -> chatFeatureToText feature <> ": " <> prefEnabledToText enabled + CISndChatFeature feature enabled -> chatFeatureToText feature <> ": " <> prefEnabledToText enabled + CIRcvChatFeatureRejected feature -> chatFeatureToText feature <> ": received, prohibited" msgIntegrityError :: MsgErrorType -> Text msgIntegrityError = \case @@ -759,6 +765,9 @@ data JSONCIContent | JCISndGroupEvent {sndGroupEvent :: SndGroupEvent} | JCIRcvConnEvent {rcvConnEvent :: RcvConnEvent} | JCISndConnEvent {sndConnEvent :: SndConnEvent} + | JCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled} + | JCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled} + | JCIRcvChatFeatureRejected {feature :: ChatFeature} deriving (Generic) instance FromJSON JSONCIContent where @@ -783,6 +792,9 @@ jsonCIContent = \case CISndGroupEvent sndGroupEvent -> JCISndGroupEvent {sndGroupEvent} CIRcvConnEvent rcvConnEvent -> JCIRcvConnEvent {rcvConnEvent} CISndConnEvent sndConnEvent -> JCISndConnEvent {sndConnEvent} + CIRcvChatFeature feature enabled -> JCIRcvChatFeature {feature, enabled} + CISndChatFeature feature enabled -> JCISndChatFeature {feature, enabled} + CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature} aciContentJSON :: JSONCIContent -> ACIContent aciContentJSON = \case @@ -799,6 +811,9 @@ aciContentJSON = \case JCISndGroupEvent {sndGroupEvent} -> ACIContent SMDSnd $ CISndGroupEvent sndGroupEvent JCIRcvConnEvent {rcvConnEvent} -> ACIContent SMDRcv $ CIRcvConnEvent rcvConnEvent JCISndConnEvent {sndConnEvent} -> ACIContent SMDSnd $ CISndConnEvent sndConnEvent + JCIRcvChatFeature {feature, enabled} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled + JCISndChatFeature {feature, enabled} -> ACIContent SMDSnd $ CISndChatFeature feature enabled + JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature -- platform independent data DBJSONCIContent @@ -815,6 +830,9 @@ data DBJSONCIContent | DBJCISndGroupEvent {sndGroupEvent :: DBSndGroupEvent} | DBJCIRcvConnEvent {rcvConnEvent :: DBRcvConnEvent} | DBJCISndConnEvent {sndConnEvent :: DBSndConnEvent} + | DBJCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled} + | DBJCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled} + | DBJCIRcvChatFeatureRejected {feature :: ChatFeature} deriving (Generic) instance FromJSON DBJSONCIContent where @@ -839,6 +857,9 @@ dbJsonCIContent = \case CISndGroupEvent sge -> DBJCISndGroupEvent $ SGE sge CIRcvConnEvent rce -> DBJCIRcvConnEvent $ RCE rce CISndConnEvent sce -> DBJCISndConnEvent $ SCE sce + CIRcvChatFeature feature enabled -> DBJCIRcvChatFeature {feature, enabled} + CISndChatFeature feature enabled -> DBJCISndChatFeature {feature, enabled} + CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature} aciContentDBJSON :: DBJSONCIContent -> ACIContent aciContentDBJSON = \case @@ -855,6 +876,9 @@ aciContentDBJSON = \case DBJCISndGroupEvent (SGE sge) -> ACIContent SMDSnd $ CISndGroupEvent sge DBJCIRcvConnEvent (RCE rce) -> ACIContent SMDRcv $ CIRcvConnEvent rce DBJCISndConnEvent (SCE sce) -> ACIContent SMDSnd $ CISndConnEvent sce + DBJCIRcvChatFeature {feature, enabled} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled + DBJCISndChatFeature {feature, enabled} -> ACIContent SMDSnd $ CISndChatFeature feature enabled + DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature data CICallStatus = CISCallPending diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 437cb9cb52..21ca5996c6 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -43,7 +43,7 @@ import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Generics (Generic) import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON) +import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Protocol (SMPServerWithAuth) import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) @@ -51,16 +51,27 @@ class IsContact a where contactId' :: a -> ContactId profile' :: a -> LocalProfile localDisplayName' :: a -> ContactName + preferences' :: a -> Maybe Preferences instance IsContact User where contactId' = userContactId + {-# INLINE contactId' #-} profile' = profile + {-# INLINE profile' #-} localDisplayName' = localDisplayName + {-# INLINE localDisplayName' #-} + preferences' User {profile = LocalProfile {preferences}} = preferences + {-# INLINE preferences' #-} instance IsContact Contact where contactId' = contactId + {-# INLINE contactId' #-} profile' = profile + {-# INLINE profile' #-} localDisplayName' = localDisplayName + {-# INLINE localDisplayName' #-} + preferences' Contact {profile = LocalProfile {preferences}} = preferences + {-# INLINE preferences' #-} data User = User { userId :: UserId, @@ -239,6 +250,19 @@ data ChatFeature = CFFullDelete | -- | CFReceipts CFVoice + deriving (Show, Generic) + +chatFeatureToText :: ChatFeature -> Text +chatFeatureToText = \case + CFFullDelete -> "Full deletion" + CFVoice -> "Voice messages" + +instance ToJSON ChatFeature where + toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CF" + toJSON = J.genericToJSON . enumJSON $ dropPrefix "CF" + +instance FromJSON ChatFeature where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CF" allChatFeatures :: [ChatFeature] allChatFeatures = @@ -259,17 +283,6 @@ chatPrefName = \case -- CFReceipts -> "delivery receipts" CFVoice -> "voice messages" -class HasPreferences p where - preferences' :: p -> Maybe Preferences - -instance HasPreferences User where - preferences' User {profile = LocalProfile {preferences}} = preferences - {-# INLINE preferences' #-} - -instance HasPreferences Contact where - preferences' Contact {profile = LocalProfile {preferences}} = preferences - {-# INLINE preferences' #-} - class PreferenceI p where getPreference :: ChatFeature -> p -> Preference @@ -515,7 +528,7 @@ mergeGroupPreferences groupPreferences = pref pt = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPreferences >>= groupPrefSel pt) data PrefEnabled = PrefEnabled {forUser :: Bool, forContact :: Bool} - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, FromJSON) instance ToJSON PrefEnabled where toJSON = J.genericToJSON J.defaultOptions @@ -529,6 +542,17 @@ prefEnabled Preference {allow = user} Preference {allow = contact} = case (user, (FANo, _) -> PrefEnabled False False _ -> PrefEnabled True True +prefEnabledToText :: PrefEnabled -> Text +prefEnabledToText = \case + PrefEnabled True True -> "enabled" + PrefEnabled False False -> "off" + PrefEnabled {forUser = True, forContact = False} -> "enabled for you" + PrefEnabled {forUser = False, forContact = True} -> "enabled for contact" + +contactUserPreferences' :: User -> Contact -> ContactUserPreferences +contactUserPreferences' user ct = + contactUserPreferences user (userPreferences ct) (preferences' ct) (contactConnIncognito ct) + contactUserPreferences :: User -> Preferences -> Maybe Preferences -> Bool -> ContactUserPreferences contactUserPreferences user userPreferences contactPreferences connectedIncognito = ContactUserPreferences diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 641da2467f..c5901d5e2f 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -745,7 +745,7 @@ viewContactPreferences user ct ct' cups = viewContactPref :: FullPreferences -> FullPreferences -> Maybe Preferences -> ContactUserPreferences -> ChatFeature -> Maybe StyledString viewContactPref userPrefs userPrefs' ctPrefs cups pt | userPref == userPref' && ctPref == contactPreference = Nothing - | otherwise = Just $ plain (chatPrefName pt) <> ": " <> viewPrefEnabled enabled <> " (you allow: " <> viewCountactUserPref userPreference <> ", contact allows: " <> viewPreference contactPreference <> ")" + | otherwise = Just $ plain (chatPrefName pt) <> ": " <> plain (prefEnabledToText enabled) <> " (you allow: " <> viewCountactUserPref userPreference <> ", contact allows: " <> viewPreference contactPreference <> ")" where userPref = getPreference pt userPrefs userPref' = getPreference pt userPrefs' @@ -776,13 +776,6 @@ viewCountactUserPref = \case CUPUser p -> "default (" <> viewPreference p <> ")" CUPContact p -> viewPreference p -viewPrefEnabled :: PrefEnabled -> StyledString -viewPrefEnabled = \case - PrefEnabled True True -> "enabled" - PrefEnabled False False -> "off" - PrefEnabled {forUser = True, forContact = False} -> "enabled for you" - PrefEnabled {forUser = False, forContact = True} -> "enabled for contact" - viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> [StyledString] viewGroupUpdated GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, image, groupPreferences = gps}} diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index d2003ef67b..9a48036f30 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -260,23 +260,23 @@ testAddContact = versionTestMatrix2 runTestAddContact bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY") bob #$> ("/_get chat @2 count=100", chat, []) chatsEmpty alice bob = do - alice @@@ [("@bob", "")] - alice #$> ("/_get chat @2 count=100", chat, []) - bob @@@ [("@alice", "")] - bob #$> ("/_get chat @2 count=100", chat, []) + alice @@@ [("@bob", "Voice messages: enabled")] + alice #$> ("/_get chat @2 count=100", chat, chatFeatures) + bob @@@ [("@alice", "Voice messages: enabled")] + bob #$> ("/_get chat @2 count=100", chat, chatFeatures) chatsOneMessage alice bob = do alice @@@ [("@bob", "hello there 🙂")] - alice #$> ("/_get chat @2 count=100", chat, [(1, "hello there 🙂")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hello there 🙂")]) bob @@@ [("@alice", "hello there 🙂")] - bob #$> ("/_get chat @2 count=100", chat, [(0, "hello there 🙂")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hello there 🙂")]) chatsManyMessages alice bob = do alice @@@ [("@bob", "how are you?")] - alice #$> ("/_get chat @2 count=100", chat, [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) bob @@@ [("@alice", "how are you?")] - bob #$> ("/_get chat @2 count=100", chat, [(0, "hello there 🙂"), (1, "hello there"), (1, "how are you?")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hello there 🙂"), (1, "hello there"), (1, "how are you?")]) -- pagination - alice #$> ("/_get chat @2 after=1 count=100", chat, [(0, "hello there"), (0, "how are you?")]) - alice #$> ("/_get chat @2 before=2 count=100", chat, [(1, "hello there 🙂")]) + alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")]) + alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, chatFeatures <> [(1, "hello there 🙂")]) -- search alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) -- read messages @@ -327,14 +327,14 @@ testDirectMessageUpdate = alice <# "bob> > hello 🙂" alice <## " hi alice" - alice #$> ("/_get chat @2 count=100", chat', [((1, "hello 🙂"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂"))]) - bob #$> ("/_get chat @2 count=100", chat', [((0, "hello 🙂"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂"))]) + alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "hello 🙂"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂"))]) + bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂"))]) - alice #$> ("/_update item @2 1 text hey 👋", id, "message updated") + alice #$> ("/_update item @2 " <> itemId 1 <> " text hey 👋", id, "message updated") bob <# "alice> [edited] hey 👋" - alice #$> ("/_get chat @2 count=100", chat', [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂"))]) - bob #$> ("/_get chat @2 count=100", chat', [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂"))]) + alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂"))]) + bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂"))]) -- msg id 3 bob `send` "> @alice (hey) hey alice" @@ -343,27 +343,27 @@ testDirectMessageUpdate = alice <# "bob> > hey 👋" alice <## " hey alice" - alice #$> ("/_get chat @2 count=100", chat', [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂")), ((0, "hey alice"), Just (1, "hey 👋"))]) - bob #$> ("/_get chat @2 count=100", chat', [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂")), ((1, "hey alice"), Just (0, "hey 👋"))]) + alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂")), ((0, "hey alice"), Just (1, "hey 👋"))]) + bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂")), ((1, "hey alice"), Just (0, "hey 👋"))]) - alice #$> ("/_update item @2 1 text greetings 🤝", id, "message updated") + alice #$> ("/_update item @2 " <> itemId 1 <> " text greetings 🤝", id, "message updated") bob <# "alice> [edited] greetings 🤝" - alice #$> ("/_update item @2 2 text updating bob's message", id, "cannot update this item") + alice #$> ("/_update item @2 " <> itemId 2 <> " text updating bob's message", id, "cannot update this item") - alice #$> ("/_get chat @2 count=100", chat', [((1, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂")), ((0, "hey alice"), Just (1, "hey 👋"))]) - bob #$> ("/_get chat @2 count=100", chat', [((0, "greetings 🤝"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂")), ((1, "hey alice"), Just (0, "hey 👋"))]) + alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂")), ((0, "hey alice"), Just (1, "hey 👋"))]) + bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "greetings 🤝"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂")), ((1, "hey alice"), Just (0, "hey 👋"))]) - bob #$> ("/_update item @2 2 text hey Alice", id, "message updated") + bob #$> ("/_update item @2 " <> itemId 2 <> " text hey Alice", id, "message updated") alice <# "bob> [edited] > hello 🙂" alice <## " hey Alice" - bob #$> ("/_update item @2 3 text greetings Alice", id, "message updated") + bob #$> ("/_update item @2 " <> itemId 3 <> " text greetings Alice", id, "message updated") alice <# "bob> [edited] > hey 👋" alice <## " greetings Alice" - alice #$> ("/_get chat @2 count=100", chat', [((1, "greetings 🤝"), Nothing), ((0, "hey Alice"), Just (1, "hello 🙂")), ((0, "greetings Alice"), Just (1, "hey 👋"))]) - bob #$> ("/_get chat @2 count=100", chat', [((0, "greetings 🤝"), Nothing), ((1, "hey Alice"), Just (0, "hello 🙂")), ((1, "greetings Alice"), Just (0, "hey 👋"))]) + alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "greetings 🤝"), Nothing), ((0, "hey Alice"), Just (1, "hello 🙂")), ((0, "greetings Alice"), Just (1, "hey 👋"))]) + bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "greetings 🤝"), Nothing), ((1, "hey Alice"), Just (0, "hello 🙂")), ((1, "greetings Alice"), Just (0, "hey 👋"))]) testDirectMessageDelete :: IO () testDirectMessageDelete = @@ -383,28 +383,28 @@ testDirectMessageDelete = alice <## " hey alic" -- alice: deletes msg ids 1,2 - alice #$> ("/_delete item @2 1 internal", id, "message deleted") - alice #$> ("/_delete item @2 2 internal", id, "message deleted") + alice #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted") + alice #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted") - alice @@@ [("@bob", "")] - alice #$> ("/_get chat @2 count=100", chat, []) + alice @@@ [("@bob", "Voice messages: enabled")] + alice #$> ("/_get chat @2 count=100", chat, chatFeatures) -- alice: msg id 1 - bob #$> ("/_update item @2 2 text hey alice", id, "message updated") + bob #$> ("/_update item @2 " <> itemId 2 <> " text hey alice", id, "message updated") alice <# "bob> [edited] hey alice" alice @@@ [("@bob", "hey alice")] - alice #$> ("/_get chat @2 count=100", chat, [(0, "hey alice")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice")]) -- bob: deletes msg id 2 - bob #$> ("/_delete item @2 2 broadcast", id, "message deleted") + bob #$> ("/_delete item @2 " <> itemId 2 <> " broadcast", id, "message deleted") alice <# "bob> [deleted] hey alice" alice @@@ [("@bob", "this item is deleted (broadcast)")] - alice #$> ("/_get chat @2 count=100", chat, [(0, "this item is deleted (broadcast)")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "this item is deleted (broadcast)")]) -- alice: deletes msg id 1 that was broadcast deleted by bob - alice #$> ("/_delete item @2 1 internal", id, "message deleted") - alice @@@ [("@bob", "")] - alice #$> ("/_get chat @2 count=100", chat, []) + alice #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted") + alice @@@ [("@bob", "Voice messages: enabled")] + alice #$> ("/_get chat @2 count=100", chat, chatFeatures) -- alice: msg id 1, bob: msg id 2 (quoting message alice deleted locally) bob `send` "> @alice (hello 🙂) do you receive my messages?" @@ -413,24 +413,24 @@ testDirectMessageDelete = alice <# "bob> > hello 🙂" alice <## " do you receive my messages?" alice @@@ [("@bob", "do you receive my messages?")] - alice #$> ("/_get chat @2 count=100", chat', [((0, "do you receive my messages?"), Just (1, "hello 🙂"))]) - alice #$> ("/_delete item @2 1 broadcast", id, "cannot delete this item") + alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))]) + alice #$> ("/_delete item @2 " <> itemId 1 <> " broadcast", id, "cannot delete this item") -- alice: msg id 2, bob: msg id 3 bob #> "@alice how are you?" alice <# "bob> how are you?" -- alice: deletes msg id 2 - alice #$> ("/_delete item @2 2 internal", id, "message deleted") + alice #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted") -- bob: deletes msg id 3 (that alice deleted locally) - bob #$> ("/_delete item @2 3 broadcast", id, "message deleted") + bob #$> ("/_delete item @2 " <> itemId 3 <> " broadcast", id, "message deleted") alice <## "bob> [deleted - original message not found]" alice @@@ [("@bob", "do you receive my messages?")] - alice #$> ("/_get chat @2 count=100", chat', [((0, "do you receive my messages?"), Just (1, "hello 🙂"))]) + alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))]) bob @@@ [("@alice", "do you receive my messages?")] - bob #$> ("/_get chat @2 count=100", chat', [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))]) + bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))]) testGroup :: Spec testGroup = versionTestMatrix3 runTestGroup @@ -557,8 +557,8 @@ testGroupShared alice bob cath checkMessages = do alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) -- "before" and "after" define a chat item id across all chats, -- so we take into account group event items as well as sent group invitations in direct chats - alice #$> ("/_get chat #1 after=5 count=100", chat, [(0, "hi there"), (0, "hey team")]) - alice #$> ("/_get chat #1 before=7 count=100", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) + alice #$> ("/_get chat #1 after=" <> groupItemId 2 5 <> " count=100", chat, [(0, "hi there"), (0, "hey team")]) + alice #$> ("/_get chat #1 before=" <> groupItemId 2 7 <> " count=100", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")]) bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] bob #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")]) @@ -683,7 +683,7 @@ testGroup2 = <##? [ "dan> hi", "@dan hey" ] - alice ##> "/t 12" + alice ##> "/t 18" alice <##? [ "@bob sent invitation to join group club as admin", "@cath sent invitation to join group club as admin", @@ -696,7 +696,13 @@ testGroup2 = "#club cath> hey", "#club dan> how is it going?", "dan> hi", - "@dan hey" + "@dan hey", + "dan> Full deletion: off", + "dan> Voice messages: enabled", + "bob> Full deletion: off", + "bob> Voice messages: enabled", + "cath> Full deletion: off", + "cath> Voice messages: enabled" ] -- remove member cath ##> "/rm club dan" @@ -1157,7 +1163,7 @@ testGroupMessageUpdate = (bob <# "#team alice> hello!") (cath <# "#team alice> hello!") - alice #$> ("/_update item #1 5 text hey 👋", id, "message updated") + alice #$> ("/_update item #1 " <> groupItemId 2 5 <> " text hey 👋", id, "message updated") concurrently_ (bob <# "#team alice> [edited] hey 👋") (cath <# "#team alice> [edited] hey 👋") @@ -1185,12 +1191,12 @@ testGroupMessageUpdate = bob #$> ("/_get chat #1 count=2", chat', [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hey 👋"))]) cath #$> ("/_get chat #1 count=2", chat', [((0, "hey 👋"), Nothing), ((0, "hi alice"), Just (0, "hey 👋"))]) - alice #$> ("/_update item #1 5 text greetings 🤝", id, "message updated") + alice #$> ("/_update item #1 " <> groupItemId 2 5 <> " text greetings 🤝", id, "message updated") concurrently_ (bob <# "#team alice> [edited] greetings 🤝") (cath <# "#team alice> [edited] greetings 🤝") - alice #$> ("/_update item #1 6 text updating bob's message", id, "cannot update this item") + alice #$> ("/_update item #1 " <> groupItemId 2 6 <> " text updating bob's message", id, "cannot update this item") threadDelay 1000000 cath `send` "> #team @alice (greetings) greetings!" @@ -1223,7 +1229,7 @@ testGroupMessageDelete = (cath <# "#team alice> hello!") -- alice: deletes msg id 5 - alice #$> ("/_delete item #1 5 internal", id, "message deleted") + alice #$> ("/_delete item #1 " <> groupItemId 2 5 <> " internal", id, "message deleted") alice #$> ("/_get chat #1 count=1", chat, [(0, "connected")]) bob #$> ("/_get chat #1 count=1", chat, [(0, "hello!")]) @@ -1249,14 +1255,14 @@ testGroupMessageDelete = cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))]) -- alice: deletes msg id 5 - alice #$> ("/_delete item #1 5 internal", id, "message deleted") + alice #$> ("/_delete item #1 " <> groupItemId 2 5 <> " internal", id, "message deleted") alice #$> ("/_get chat #1 count=1", chat', [((0, "connected"), Nothing)]) bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alic"), Just (0, "hello!"))]) cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))]) -- alice: msg id 5 - bob #$> ("/_update item #1 6 text hi alice", id, "message updated") + bob #$> ("/_update item #1 " <> groupItemId 1 6 <> " text hi alice", id, "message updated") concurrently_ (alice <# "#team bob> [edited] hi alice") ( do @@ -1275,13 +1281,13 @@ testGroupMessageDelete = (alice <# "#team cath> how are you?") (bob <# "#team cath> how are you?") - cath #$> ("/_delete item #1 6 broadcast", id, "message deleted") + cath #$> ("/_delete item #1 " <> groupItemId 1 6 <> " broadcast", id, "message deleted") concurrently_ (alice <# "#team cath> [deleted] how are you?") (bob <# "#team cath> [deleted] how are you?") - alice #$> ("/_delete item #1 5 broadcast", id, "cannot delete this item") - alice #$> ("/_delete item #1 5 internal", id, "message deleted") + alice #$> ("/_delete item #1 " <> groupItemId 2 5 <> " broadcast", id, "cannot delete this item") + alice #$> ("/_delete item #1 " <> groupItemId 2 5 <> " internal", id, "message deleted") alice #$> ("/_get chat #1 count=1", chat', [((0, "this item is deleted (broadcast)"), Nothing)]) bob #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "this item is deleted (broadcast)"), Nothing)]) @@ -1839,8 +1845,8 @@ runTestMessageWithFile alice bob = do src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src - alice #$> ("/_get chat @2 count=100", chatF, [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")]) - bob #$> ("/_get chat @2 count=100", chatF, [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")]) + alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")]) + bob #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")]) testSendImage :: IO () testSendImage = @@ -1863,8 +1869,8 @@ testSendImage = src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src - alice #$> ("/_get chat @2 count=100", chatF, [((1, ""), Just "./tests/fixtures/test.jpg")]) - bob #$> ("/_get chat @2 count=100", chatF, [((0, ""), Just "./tests/tmp/test.jpg")]) + alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, ""), Just "./tests/fixtures/test.jpg")]) + bob #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((0, ""), Just "./tests/tmp/test.jpg")]) -- deleting contact without files folder set should not remove file bob ##> "/d alice" bob <## "alice: contact is deleted" @@ -1894,8 +1900,8 @@ testFilesFoldersSendImage = src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/app_files/test.jpg" dest `shouldBe` src - alice #$> ("/_get chat @2 count=100", chatF, [((1, ""), Just "test.jpg")]) - bob #$> ("/_get chat @2 count=100", chatF, [((0, ""), Just "test.jpg")]) + alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, ""), Just "test.jpg")]) + bob #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((0, ""), Just "test.jpg")]) -- deleting contact with files folder set should remove file checkActionDeletesFile "./tests/tmp/app_files/test.jpg" $ do bob ##> "/d alice" @@ -1964,7 +1970,7 @@ testSendImageWithTextAndQuote = connectUsers alice bob bob #> "@alice hi alice" alice <# "bob> hi alice" - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": 1, \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 1 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") alice <# "@bob > hi alice" alice <## " hey bob" alice <# "/f @bob ./tests/fixtures/test.jpg" @@ -1983,12 +1989,12 @@ testSendImageWithTextAndQuote = (alice <## "completed sending file 1 (test.jpg) to bob") src <- B.readFile "./tests/fixtures/test.jpg" B.readFile "./tests/tmp/test.jpg" `shouldReturn` src - alice #$> ("/_get chat @2 count=100", chat'', [((0, "hi alice"), Nothing, Nothing), ((1, "hey bob"), Just (0, "hi alice"), Just "./tests/fixtures/test.jpg")]) + alice #$> ("/_get chat @2 count=100", chat'', chatFeatures'' <> [((0, "hi alice"), Nothing, Nothing), ((1, "hey bob"), Just (0, "hi alice"), Just "./tests/fixtures/test.jpg")]) alice @@@ [("@bob", "hey bob")] - bob #$> ("/_get chat @2 count=100", chat'', [((1, "hi alice"), Nothing, Nothing), ((0, "hey bob"), Just (1, "hi alice"), Just "./tests/tmp/test.jpg")]) + bob #$> ("/_get chat @2 count=100", chat'', chatFeatures'' <> [((1, "hi alice"), Nothing, Nothing), ((0, "hey bob"), Just (1, "hi alice"), Just "./tests/tmp/test.jpg")]) bob @@@ [("@alice", "hey bob")] -- quoting (file + text) with file uses quoted text - bob ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.txt\", \"quotedItemId\": 2, \"msgContent\": {\"text\":\"\",\"type\":\"file\"}}" + bob ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.txt\", \"quotedItemId\": " <> itemId 2 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"file\"}}") bob <# "@alice > hey bob" bob <## " test.txt" bob <# "/f @alice ./tests/fixtures/test.txt" @@ -2008,7 +2014,7 @@ testSendImageWithTextAndQuote = txtSrc <- B.readFile "./tests/fixtures/test.txt" B.readFile "./tests/tmp/test.txt" `shouldReturn` txtSrc -- quoting (file without text) with file uses file name - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": 3, \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 3 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") alice <# "@bob > test.txt" alice <## " test.jpg" alice <# "/f @bob ./tests/fixtures/test.jpg" @@ -2084,7 +2090,7 @@ testGroupSendImageWithTextAndQuote = (alice <# "#team bob> hi team") (cath <# "#team bob> hi team") threadDelay 1000000 - alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": 5, \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> ("/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> groupItemId 2 5 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") alice <# "#team > bob hi team" alice <## " hey bob" alice <# "/f #team ./tests/fixtures/test.jpg" @@ -2145,7 +2151,7 @@ testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") - alice @@@ [("@bob", "")] + alice @@@ [("@bob", "Voice messages: enabled")] alice <##> bob cath ##> ("/c " <> cLink) @@ -2156,7 +2162,7 @@ testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") - alice @@@ [("@cath", ""), ("@bob", "hey")] + alice @@@ [("@cath", "Voice messages: enabled"), ("@bob", "hey")] alice <##> cath testUserContactLinkAutoAccept :: IO () @@ -2174,7 +2180,7 @@ testUserContactLinkAutoAccept = concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") - alice @@@ [("@bob", "")] + alice @@@ [("@bob", "Voice messages: enabled")] alice <##> bob alice ##> "/auto_accept on" @@ -2186,7 +2192,7 @@ testUserContactLinkAutoAccept = concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") - alice @@@ [("@cath", ""), ("@bob", "hey")] + alice @@@ [("@cath", "Voice messages: enabled"), ("@bob", "hey")] alice <##> cath alice ##> "/auto_accept off" @@ -2200,7 +2206,7 @@ testUserContactLinkAutoAccept = concurrently_ (dan <## "alice (Alice): contact is connected") (alice <## "dan (Daniel): contact is connected") - alice @@@ [("@dan", ""), ("@cath", "hey"), ("@bob", "hey")] + alice @@@ [("@dan", "Voice messages: enabled"), ("@cath", "hey"), ("@bob", "hey")] alice <##> dan testDeduplicateContactRequests :: IO () @@ -2229,8 +2235,8 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ bob ##> ("/c " <> cLink) bob <## "alice (Alice): contact already exists" - alice @@@ [("@bob", "")] - bob @@@ [("@alice", ""), (":2", ""), (":1", "")] + alice @@@ [("@bob", "Voice messages: enabled")] + bob @@@ [("@alice", "Voice messages: enabled"), (":2", ""), (":1", "")] bob ##> "/_delete :1" bob <## "connection :1 deleted" bob ##> "/_delete :2" @@ -2244,8 +2250,8 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ bob <## "alice (Alice): contact already exists" alice <##> bob - alice #$> ("/_get chat @2 count=100", chat, [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")]) - bob #$> ("/_get chat @2 count=100", chat, [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")]) cath ##> ("/c " <> cLink) alice <#? cath @@ -2255,7 +2261,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") - alice @@@ [("@cath", ""), ("@bob", "hey")] + alice @@@ [("@cath", "Voice messages: enabled"), ("@bob", "hey")] alice <##> cath testDeduplicateContactRequestsProfileChange :: IO () @@ -2299,8 +2305,8 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile bob ##> ("/c " <> cLink) bob <## "alice (Alice): contact already exists" - alice @@@ [("@robert", "")] - bob @@@ [("@alice", ""), (":3", ""), (":2", ""), (":1", "")] + alice @@@ [("@robert", "Voice messages: enabled")] + bob @@@ [("@alice", "Voice messages: enabled"), (":3", ""), (":2", ""), (":1", "")] bob ##> "/_delete :1" bob <## "connection :1 deleted" bob ##> "/_delete :2" @@ -2316,8 +2322,8 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile bob <## "alice (Alice): contact already exists" alice <##> bob - alice #$> ("/_get chat @2 count=100", chat, [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")]) - bob #$> ("/_get chat @2 count=100", chat, [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")]) cath ##> ("/c " <> cLink) alice <#? cath @@ -2327,7 +2333,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") - alice @@@ [("@cath", ""), ("@robert", "hey")] + alice @@@ [("@cath", "Voice messages: enabled"), ("@robert", "hey")] alice <##> cath testRejectContactAndDeleteUserContact :: IO () @@ -2827,7 +2833,7 @@ testSetConnectionAlias = testChat2 aliceProfile bobProfile $ concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") - alice @@@ [("@bob", "")] + alice @@@ [("@bob", "Voice messages: enabled")] alice ##> "/cs" alice <## "bob (Bob) (alias: friend)" @@ -3272,45 +3278,45 @@ testNegotiateCall = -- alice invite bob to call alice ##> ("/_call invite @2 " <> serialize testCallType) alice <## "ok" - alice #$> ("/_get chat @2 count=100", chat, [(1, "outgoing call: calling...")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: calling...")]) bob <## "alice wants to connect with you via WebRTC video call (e2e encrypted)" repeatM_ 3 $ getTermLine bob - bob #$> ("/_get chat @2 count=100", chat, [(0, "incoming call: calling...")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: calling...")]) -- bob accepts call by sending WebRTC offer bob ##> ("/_call offer @2 " <> serialize testWebRTCCallOffer) bob <## "ok" - bob #$> ("/_get chat @2 count=100", chat, [(0, "incoming call: accepted")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: accepted")]) alice <## "bob accepted your WebRTC video call (e2e encrypted)" repeatM_ 3 $ getTermLine alice alice <## "message updated" -- call chat item updated - alice #$> ("/_get chat @2 count=100", chat, [(1, "outgoing call: accepted")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: accepted")]) -- alice confirms call by sending WebRTC answer alice ##> ("/_call answer @2 " <> serialize testWebRTCSession) alice <### [ "ok", "message updated" ] - alice #$> ("/_get chat @2 count=100", chat, [(1, "outgoing call: connecting...")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: connecting...")]) bob <## "alice continued the WebRTC call" repeatM_ 3 $ getTermLine bob - bob #$> ("/_get chat @2 count=100", chat, [(0, "incoming call: connecting...")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: connecting...")]) -- participants can update calls as connected alice ##> "/_call status @2 connected" alice <### [ "ok", "message updated" ] - alice #$> ("/_get chat @2 count=100", chat, [(1, "outgoing call: in progress (00:00)")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: in progress (00:00)")]) bob ##> "/_call status @2 connected" bob <## "ok" - bob #$> ("/_get chat @2 count=100", chat, [(0, "incoming call: in progress (00:00)")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: in progress (00:00)")]) -- either party can end the call bob ##> "/_call end @2" bob <## "ok" - bob #$> ("/_get chat @2 count=100", chat, [(0, "incoming call: ended (00:00)")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: ended (00:00)")]) alice <## "call with bob ended" alice <## "message updated" - alice #$> ("/_get chat @2 count=100", chat, [(1, "outgoing call: ended (00:00)")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: ended (00:00)")]) testMaintenanceMode :: IO () testMaintenanceMode = withTmpFiles $ do @@ -3501,11 +3507,11 @@ testSetChatItemTTL = bob <# "alice> 3" bob #> "@alice 4" alice <# "bob> 4" - alice #$> ("/_get chat @2 count=100", chatF, [((1, "1"), Nothing), ((0, "2"), Nothing), ((1, ""), Just "test.jpg"), ((1, "3"), Nothing), ((0, "4"), Nothing)]) + alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "1"), Nothing), ((0, "2"), Nothing), ((1, ""), Just "test.jpg"), ((1, "3"), Nothing), ((0, "4"), Nothing)]) checkActionDeletesFile "./tests/tmp/app_files/test.jpg" $ alice #$> ("/_ttl 2", id, "ok") alice #$> ("/_get chat @2 count=100", chat, [(1, "3"), (0, "4")]) -- when expiration is turned on, first cycle is synchronous - bob #$> ("/_get chat @2 count=100", chat, [(0, "1"), (1, "2"), (0, ""), (0, "3"), (1, "4")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "1"), (1, "2"), (0, ""), (0, "3"), (1, "4")]) alice #$> ("/ttl", id, "old messages are set to be deleted after: 2 second(s)") alice #$> ("/ttl week", id, "ok") alice #$> ("/ttl", id, "old messages are set to be deleted after: one week") @@ -3549,8 +3555,8 @@ testGroupLink = alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected")] -- calling /_get chat api marks it as used and adds it to chat previews - alice #$> ("/_get chat @2 count=100", chat, []) - alice @@@ [("@bob", ""), ("#team", "connected")] + alice #$> ("/_get chat @2 count=100", chat, chatFeatures) + alice @@@ [("@bob", "Voice messages: enabled"), ("#team", "connected")] alice <##> bob alice @@@ [("@bob", "hey"), ("#team", "connected")] @@ -3830,8 +3836,8 @@ testSwitchContact = alice <## "bob: you started changing address" bob <## "alice changed address for you" alice <## "bob: you changed address" - alice #$> ("/_get chat @2 count=100", chat, [(1, "started changing address..."), (1, "you changed address")]) - bob #$> ("/_get chat @2 count=100", chat, [(0, "started changing address for you..."), (0, "changed address for you")]) + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "started changing address..."), (1, "you changed address")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "changed address for you")]) alice <##> bob testSwitchGroupMember :: IO () @@ -4021,6 +4027,24 @@ chatF = map (\(a, _, c) -> (a, c)) . chat'' chat'' :: String -> [((Int, String), Maybe (Int, String), Maybe String)] chat'' = read +chatFeatures :: [(Int, String)] +chatFeatures = map (\(a, _, _) -> a) chatFeatures'' + +chatFeatures' :: [((Int, String), Maybe (Int, String))] +chatFeatures' = map (\(a, b, _) -> (a, b)) chatFeatures'' + +chatFeaturesF :: [((Int, String), Maybe String)] +chatFeaturesF = map (\(a, _, c) -> (a, c)) chatFeatures'' + +chatFeatures'' :: [((Int, String), Maybe (Int, String), Maybe String)] +chatFeatures'' = [((0, "Full deletion: off"), Nothing, Nothing), ((0, "Voice messages: enabled"), Nothing, Nothing)] + +itemId :: Int -> String +itemId i = show $ length chatFeatures + i + +groupItemId :: Int -> Int -> String +groupItemId n i = show $ (length chatFeatures) * n + i + (@@@) :: TestCC -> [(String, String)] -> Expectation (@@@) = getChats . map $ \(ldn, msg, _) -> (ldn, msg)