core: chat items showing preference changes (#1399)

This commit is contained in:
Evgeny Poberezkin
2022-11-22 12:50:56 +00:00
committed by GitHub
parent c7f70f0ed0
commit 2b09fb425d
11 changed files with 297 additions and 138 deletions

View File

@@ -9,7 +9,7 @@
import SwiftUI
import SimpleXChat
func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
func infoRow<S>(_ title: S, _ value: String) -> some View where S: StringProtocol {
HStack {
Text(title)
Spacer()

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 = "<group>"; };
5C55A92D283D0FDE00C4E99E /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = sounds; sourceTree = "<group>"; };
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = "<group>"; };
5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatFeatureView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
5C58BCCC292AA93700AF9E4F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a"; sourceTree = "<group>"; };
@@ -672,6 +674,7 @@
5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */,
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */,
6440C9FF288857A10062C672 /* CIEventView.swift */,
5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */,
);
path = ChatItem;
sourceTree = "<group>";
@@ -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 */,

View File

@@ -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)
}
}
}

View File

@@ -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 =

View File

@@ -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

View File

@@ -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

View File

@@ -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}}

View File

@@ -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)