diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 291299bc15..cc4f7046ef 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -460,6 +460,7 @@ class Contact( val profile: Profile, val activeConn: Connection, val viaGroup: Long? = null, +// val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { @@ -540,6 +541,7 @@ class GroupInfo ( override val localDisplayName: String, val groupProfile: GroupProfile, val membership: GroupMember, + // val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant ): SomeChat, NamedChat { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 7700bd8ef0..fe5008e4db 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1136,6 +1136,7 @@ sealed class CC { class SetUserSMPServers(val smpServers: List): CC() class APISetNetworkConfig(val networkConfig: NetCfg): CC() class APIGetNetworkConfig: CC() + class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC() class APIContactInfo(val contactId: Long): CC() class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC() class AddContact: CC() @@ -1186,6 +1187,7 @@ sealed class CC { is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" is APIGetNetworkConfig -> "/network" + is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}" is APIContactInfo -> "/_info @$contactId" is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId" is AddContact -> "/connect" @@ -1237,6 +1239,7 @@ sealed class CC { is SetUserSMPServers -> "setUserSMPServers" is APISetNetworkConfig -> "/apiSetNetworkConfig" is APIGetNetworkConfig -> "/apiGetNetworkConfig" + is APISetChatSettings -> "/apiSetChatSettings" is APIContactInfo -> "apiContactInfo" is APIGroupMemberInfo -> "apiGroupMemberInfo" is AddContact -> "addContact" @@ -1348,6 +1351,11 @@ data class KeepAliveOpts( } } +@Serializable +data class ChatSettings( + val enableNtfs: Boolean +) + val json = Json { prettyPrint = true ignoreUnknownKeys = true diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index b0bc469594..64ee7b6970 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -45,6 +45,7 @@ public enum ChatCommand { case setUserSMPServers(smpServers: [String]) case apiSetNetworkConfig(networkConfig: NetCfg) case apiGetNetworkConfig + case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) case apiContactInfo(contactId: Int64) case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) case addContact @@ -108,6 +109,7 @@ public enum ChatCommand { case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" case .apiGetNetworkConfig: return "/network" + case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" case let .apiContactInfo(contactId): return "/_info @\(contactId)" case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" case .addContact: return "/connect" @@ -170,6 +172,7 @@ public enum ChatCommand { case .setUserSMPServers: return "setUserSMPServers" case .apiSetNetworkConfig: return "apiSetNetworkConfig" case .apiGetNetworkConfig: return "apiGetNetworkConfig" + case .apiSetChatSettings: return "apiSetChatSettings" case .apiContactInfo: return "apiContactInfo" case .apiGroupMemberInfo: return "apiGroupMemberInfo" case .addContact: return "addContact" @@ -591,6 +594,12 @@ public struct KeepAliveOpts: Codable, Equatable { public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4) } +public struct ChatSettings: Codable { + public var enableNtfs: Bool + + public static let defaults: ChatSettings = ChatSettings(enableNtfs: true) +} + public struct ConnectionStats: Codable { public var rcvServers: [String]? public var sndServers: [String]? diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index d51e8f911d..fd82605e65 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -244,6 +244,7 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var profile: Profile public var activeConn: Connection public var viaGroup: Int64? +// public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date @@ -433,6 +434,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { var localDisplayName: GroupName public var groupProfile: GroupProfile public var membership: GroupMember +// public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date diff --git a/cabal.project b/cabal.project index b8beece48c..e73877f169 100644 --- a/cabal.project +++ b/cabal.project @@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 3fc9afbb351033413dd84344ec603910e370a8ec + tag: a33539bef9d31f5acbf9a21bfa0ef39a2dd421dd source-repository-package type: git diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 33be0c784c..cdd4cdf838 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -44,6 +44,7 @@ library Simplex.Chat.Migrations.M20220715_groups_chat_item_id Simplex.Chat.Migrations.M20220811_chat_items_indices Simplex.Chat.Migrations.M20220812_incognito_profiles + Simplex.Chat.Migrations.M20220818_chat_notifications Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index eba26284ec..a28f43f61f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -261,7 +261,7 @@ processChatCommand = \case setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd)) setupSndFileTransfer ct = forM file_ $ \file -> do (fileSize, chSize) <- checkSndFile file - (agentConnId, fileConnReq) <- withAgent (`createConnection` SCMInvitation) + (agentConnId, fileConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Just fileConnReq} fileId <- withStore' $ \db -> createSndFileTransfer db userId ct file fileInvitation agentConnId chSize @@ -597,6 +597,23 @@ processChatCommand = \case pure CRCmdOk APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) $> CRCmdOk APIGetNetworkConfig -> CRNetworkConfig <$> withUser' (\_ -> withAgent getNetworkConfig) + APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user@User {userId} -> case cType of + CTDirect -> do + ct <- withStore $ \db -> do + ct <- getContact db userId chatId + liftIO $ updateContactSettings db user chatId chatSettings + pure ct + withAgent $ \a -> toggleConnectionNtfs a (contactConnId ct) (enableNtfs chatSettings) + pure CRCmdOk + CTGroup -> do + ms <- withStore $ \db -> do + Group _ ms <- getGroup db user chatId + liftIO $ updateGroupSettings db user chatId chatSettings + pure ms + withAgent $ \a -> forM_ ms $ \m -> + forM_ (memberConnId m) $ \connId -> toggleConnectionNtfs a connId (enableNtfs chatSettings) + pure CRCmdOk + _ -> pure $ chatCmdError "not supported" APIContactInfo contactId -> withUser $ \User {userId} -> do -- [incognito] print user's incognito profile for this contact ct@Contact {activeConn = Connection {customUserProfileId}} <- withStore $ \db -> getContact db userId contactId @@ -621,7 +638,7 @@ processChatCommand = \case -- [incognito] generate profile for connection incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - (connId, cReq) <- withAgent (`createConnection` SCMInvitation) + (connId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation conn <- withStore' $ \db -> createDirectConnection db userId connId ConnNew incognitoProfile toView $ CRNewContactConnection conn pure $ CRInvitation cReq @@ -630,7 +647,7 @@ processChatCommand = \case incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile - connId <- withAgent $ \a -> joinConnection a cReq . directMessage $ XInfo profileToSend + connId <- withAgent $ \a -> joinConnection a True cReq . directMessage $ XInfo profileToSend conn <- withStore' $ \db -> createDirectConnection db userId connId ConnJoined incognitoProfile toView $ CRNewContactConnection conn pure CRSentConfirmation @@ -649,7 +666,7 @@ processChatCommand = \case processChatCommand $ APIClearChat (ChatRef CTDirect contactId) ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user) CreateMyAddress -> withUser $ \User {userId} -> withChatLock . procCmd $ do - (connId, cReq) <- withAgent (`createConnection` SCMContact) + (connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact withStore $ \db -> createUserContactLink db userId connId cReq pure $ CRUserContactLinkCreated cReq DeleteMyAddress -> withUser $ \user -> withChatLock $ do @@ -731,7 +748,7 @@ processChatCommand = \case case contactMember contact members of Nothing -> do gVar <- asks idsDrg - (agentConnId, cReq) <- withAgent (`createConnection` SCMInvitation) + (agentConnId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq sendInvitation member cReq Just member@GroupMember {groupMemberId, memberStatus} @@ -760,7 +777,7 @@ processChatCommand = \case else pure g -- [incognito] if membership is incognito, send its incognito profile in XGrpAcpt let incognitoProfile = if memberIncognito membership' then Just (fromLocalProfile $ memberProfile membership') else Nothing - agentConnId <- withAgent $ \a -> joinConnection a connRequest . directMessage $ XGrpAcpt (memberId (membership' :: GroupMember)) incognitoProfile + agentConnId <- withAgent $ \a -> joinConnection a True connRequest . directMessage $ XGrpAcpt (memberId (membership' :: GroupMember)) incognitoProfile withStore' $ \db -> do createMemberConnection db userId fromMember agentConnId updateGroupMemberStatus db userId fromMember GSMemAccepted @@ -959,7 +976,7 @@ processChatCommand = \case incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = fromMaybe profile incognitoProfile - connId <- withAgent $ \a -> joinConnection a cReq $ directMessage (XContact profileToSend $ Just xContactId) + connId <- withAgent $ \a -> joinConnection a True cReq $ directMessage (XContact profileToSend $ Just xContactId) conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile toView $ CRNewContactConnection conn pure $ CRSentInvitation incognitoProfile @@ -1108,7 +1125,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F case fileConnReq of -- direct file protocol Just connReq -> - tryError (withAgent $ \a -> joinConnection a connReq . directMessage $ XFileAcpt fName) >>= \case + tryError (withAgent $ \a -> joinConnection a True connReq . directMessage $ XFileAcpt fName) >>= \case Right agentConnId -> do filePath <- getRcvFilePath filePath_ fName withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnJoined filePath @@ -1122,7 +1139,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F case activeConn of Just conn -> do sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId - (agentConnId, fileInvConnReq) <- withAgent (`createConnection` SCMInvitation) + (agentConnId, fileInvConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation filePath <- getRcvFilePath filePath_ fName ci <- withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnNew filePath void $ sendDirectMessage conn (XFileAcptInv sharedMsgId fileInvConnReq fName) (GroupId groupId) @@ -1173,7 +1190,7 @@ acceptContactRequest User {userId, profile} UserContactRequest {agentInvitationI incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile - connId <- withAgent $ \a -> acceptContact a invId . directMessage $ XInfo profileToSend + connId <- withAgent $ \a -> acceptContact a True invId . directMessage $ XInfo profileToSend withStore' $ \db -> createAcceptedContact db userId connId cName profileId p userContactLinkId xContactId incognitoProfile agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m () @@ -1455,7 +1472,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage _ -> pure () processGroupMessage :: ACommand 'Agent -> Connection -> GroupInfo -> GroupMember -> m () - processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership} m@GroupMember {memberContactProfileId} = case agentMsg of + processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership, chatSettings} m@GroupMember {memberContactProfileId} = case agentMsg of CONF confId _ connInfo -> do ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo case memberCategory m of @@ -1497,6 +1514,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage unless (memberActive membership) $ updateGroupMemberStatus db userId membership GSMemConnected sendPendingGroupMessages m conn + unless (enableNtfs chatSettings) . withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) False case memberCategory m of GCHostMember -> do -- [incognito] chat item & event with indication that host connected incognito @@ -1886,7 +1904,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage unless cancelled $ if fName == fileName then - tryError (withAgent $ \a -> joinConnection a fileConnReq . directMessage $ XOk) >>= \case + tryError (withAgent $ \a -> joinConnection a True fileConnReq . directMessage $ XOk) >>= \case Right acId -> withStore' $ \db -> createSndGroupFileTransferConnection db userId fileId acId m Left e -> throwError e @@ -2091,8 +2109,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage if isMember memId gInfo members then messageWarning "x.grp.mem.intro ignored: member already exists" else do - (groupConnId, groupConnReq) <- withAgent (`createConnection` SCMInvitation) - (directConnId, directConnReq) <- withAgent (`createConnection` SCMInvitation) + (groupConnId, groupConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation + (directConnId, directConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation -- [incognito] direct connection with member has to be established using same incognito profile customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing newMember <- withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnId directConnId customUserProfileId @@ -2126,8 +2144,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage withStore' $ \db -> saveMemberInvitation db toMember introInv -- [incognito] send membership incognito profile, create direct connection as incognito let msg = XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) - groupConnId <- withAgent $ \a -> joinConnection a groupConnReq $ directMessage msg - directConnId <- withAgent $ \a -> joinConnection a directConnReq $ directMessage msg + groupConnId <- withAgent $ \a -> joinConnection a True groupConnReq $ directMessage msg + directConnId <- withAgent $ \a -> joinConnection a True directConnReq $ directMessage msg customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing withStore' $ \db -> createIntroToMemberContact db userId m toMember groupConnId directConnId customUserProfileId @@ -2579,6 +2597,7 @@ chatCommandP = "/_network " *> (APISetNetworkConfig <$> jsonP), ("/network " <|> "/net ") *> (APISetNetworkConfig <$> netCfgP), ("/network" <|> "/net") $> APIGetNetworkConfig, + "/_settings" *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), "/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal), "/_info @" *> (APIContactInfo <$> A.decimal), ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* optional (A.char '@') <*> displayName), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 173bd90382..416fa97b53 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -150,6 +150,7 @@ data ChatCommand | SetUserSMPServers [SMPServer] | APISetNetworkConfig NetworkConfig | APIGetNetworkConfig + | APISetChatSettings ChatRef ChatSettings | APIContactInfo ContactId | APIGroupMemberInfo GroupId GroupMemberId | ContactInfo ContactName diff --git a/src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs b/src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs new file mode 100644 index 0000000000..ffb2b15967 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20220818_chat_notifications where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20220818_chat_notifications :: Query +m20220818_chat_notifications = + [sql| +ALTER TABLE contacts ADD COLUMN enable_ntfs INTEGER; + +ALTER TABLE groups ADD COLUMN enable_ntfs INTEGER; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index ce4c6553fd..f4f7ac3697 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -54,6 +54,7 @@ is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT CHECK(updated_at NOT NULL), xcontact_id BLOB, + enable_ntfs INTEGER, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -119,7 +120,8 @@ CREATE TABLE groups( inv_queue_info BLOB, created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), - chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE SET NULL, -- received + chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE SET NULL, + enable_ntfs INTEGER, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 456bb21634..74ca466072 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -176,6 +176,8 @@ module Simplex.Chat.Store getCalls, getPendingContactConnection, deletePendingContactConnection, + updateContactSettings, + updateGroupSettings, withTransaction, ) where @@ -227,6 +229,7 @@ import Simplex.Chat.Migrations.M20220702_calls import Simplex.Chat.Migrations.M20220715_groups_chat_item_id import Simplex.Chat.Migrations.M20220811_chat_items_indices import Simplex.Chat.Migrations.M20220812_incognito_profiles +import Simplex.Chat.Migrations.M20220818_chat_notifications import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -257,7 +260,8 @@ schemaMigrations = ("20220702_calls", m20220702_calls), ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id), ("20220811_chat_items_indices", m20220811_chat_items_indices), - ("20220812_incognito_profiles", m20220812_incognito_profiles) + ("20220812_incognito_profiles", m20220812_incognito_profiles), + ("20220818_chat_notifications", m20220818_chat_notifications) ] -- | The list of migrations in ascending order by date @@ -362,7 +366,7 @@ getConnReqContactXContactId db userId cReqHash = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -445,7 +449,7 @@ createDirectContact :: DB.Connection -> UserId -> Connection -> Profile -> Excep createDirectContact db userId activeConn@Connection {connId} profile = do createdAt <- liftIO getCurrentTime (localDisplayName, contactId, profileId) <- createContact_ db userId connId profile Nothing createdAt - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, createdAt, updatedAt = createdAt} + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt, updatedAt = createdAt} createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) createContact_ db userId connId Profile {displayName, fullName, image} viaGroup currentTs = @@ -566,20 +570,22 @@ updateContact_ db userId contactId displayName newName updatedAt = do (newName, updatedAt, userId, contactId) DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (displayName, userId) -type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, UTCTime, UTCTime) +type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe Bool, UTCTime, UTCTime) toContact :: ContactRow :. ConnectionRow -> Contact -toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, createdAt, updatedAt) :. connRow) = +toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image} activeConn = toConnection connRow - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} toContactOrError :: ContactRow :. MaybeConnectionRow -> Either StoreError Contact -toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, createdAt, updatedAt) :. connRow) = +toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} in case toMaybeConnection connRow of Just activeConn -> - Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} _ -> Left $ SEContactNotReady localDisplayName -- TODO return the last connection that is ready, not any last connection @@ -759,7 +765,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -878,11 +884,11 @@ createAcceptedContact db userId agentConnId localDisplayName profileId profile u customUserProfileId <- createIncognitoProfile_ db userId createdAt incognitoProfile DB.execute db - "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?)" - (userId, localDisplayName, profileId, createdAt, createdAt, xContactId) + "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?,?)" + (userId, localDisplayName, profileId, True, createdAt, createdAt, xContactId) contactId <- insertedRowId db activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, createdAt = createdAt, updatedAt = createdAt} + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt = createdAt, updatedAt = createdAt} getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -1153,16 +1159,17 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do <$> DB.query db [sql| - SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, c.via_group, c.created_at, c.updated_at + SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, c.via_group, c.enable_ntfs, c.created_at, c.updated_at FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? |] (userId, contactId) - toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime)] -> Either StoreError Contact - toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt)] = + toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, Maybe Bool, UTCTime, UTCTime)] -> Either StoreError Contact + toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, viaGroup, enableNtfs_, createdAt, updatedAt)] = let profile = LocalProfile {profileId, displayName, fullName, image} - in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} + in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = ExceptT $ do @@ -1172,7 +1179,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1260,7 +1267,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1311,13 +1318,14 @@ createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = Except profileId <- insertedRowId db DB.execute db - "INSERT INTO groups (local_display_name, user_id, group_profile_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (ldn, userId, profileId, currentTs, currentTs) + "INSERT INTO groups (local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (ldn, userId, profileId, True, currentTs, currentTs) insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 -- TODO ldn from incognito profile membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser incognitoProfile currentTs - pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} + let chatSettings = ChatSettings {enableNtfs = True} + pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, chatSettings, createdAt = currentTs, updatedAt = currentTs} -- | creates a new group record for the group the current user was invited to, or returns an existing one createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe Profile -> ExceptT StoreError IO GroupInfo @@ -1345,12 +1353,13 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInv profileId <- insertedRowId db DB.execute db - "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (profileId, localDisplayName, connRequest, userId, currentTs, currentTs) + "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (profileId, localDisplayName, connRequest, userId, True, currentTs, currentTs) insertedRowId db _ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown fromMemberProfile currentTs membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfile currentTs - pure GroupInfo {groupId, localDisplayName, groupProfile, membership, createdAt = currentTs, updatedAt = currentTs} + let chatSettings = ChatSettings {enableNtfs = True} + pure GroupInfo {groupId, localDisplayName, groupProfile, membership, chatSettings, createdAt = currentTs, updatedAt = currentTs} createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe Profile -> UTCTime -> ExceptT StoreError IO GroupMember createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfile createdAt = do @@ -1461,7 +1470,7 @@ getUserGroupDetails db User {userId, userContactId} = <$> DB.query db [sql| - SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, m.group_member_id, g.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, mp.contact_profile_id, mp.display_name, mp.full_name, mp.image FROM groups g @@ -1477,12 +1486,13 @@ getGroupInfoByName db user gName = do gId <- getGroupIdByName db user gName getGroupInfo db user gId -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, UTCTime, UTCTime) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe Bool, UTCTime, UTCTime) :. GroupMemberRow toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo -toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, createdAt, updatedAt) :. userMemberRow) = +toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. userMemberRow) = let membership = toGroupMember userContactId userMemberRow - in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, createdAt, updatedAt} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} + in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, chatSettings, createdAt, updatedAt} getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMember db user@User {userId} groupId groupMemberId = @@ -1882,7 +1892,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1922,7 +1932,7 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = db [sql| SELECT - ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.enable_ntfs, ct.created_at, ct.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at FROM contacts ct @@ -1938,11 +1948,12 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = |] (userId, groupMemberId) where - toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, UTCTime, UTCTime) :. ConnectionRow -> Contact - toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, viaGroup, createdAt, updatedAt) :. connRow) = + toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, Maybe Bool, UTCTime, UTCTime) :. ConnectionRow -> Contact + toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, viaGroup, enableNtfs_, createdAt, updatedAt) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image} + chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_} activeConn = toConnection connRow - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, createdAt, updatedAt} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} createSndFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> ConnId -> Integer -> IO Int64 createSndFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize} acId chunkSize = do @@ -2680,7 +2691,7 @@ getDirectChatPreviews_ db User {userId} = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, @@ -2745,7 +2756,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -2875,6 +2886,14 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) +updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () +updateContactSettings db User {userId} contactId ChatSettings {enableNtfs} = + DB.execute db "UPDATE contacts SET enable_ntfs = ? WHERE user_id = ? AND contact_id = ?" (enableNtfs, userId, contactId) + +updateGroupSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () +updateGroupSettings db User {userId} groupId ChatSettings {enableNtfs} = + DB.execute db "UPDATE groups SET enable_ntfs = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, userId, groupId) + toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe Int64, UTCTime, UTCTime) -> PendingContactConnection toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, customUserProfileId, createdAt, updatedAt) = PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, customUserProfileId, createdAt, updatedAt} @@ -3012,7 +3031,7 @@ getContact db userId contactId = [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.created_at, ct.updated_at, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at @@ -3137,7 +3156,7 @@ getGroupInfo db User {userId, userContactId} groupId = [sql| SELECT -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.created_at, g.updated_at, + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 95d01d2a20..141936838a 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -76,6 +76,7 @@ data Contact = Contact profile :: LocalProfile, activeConn :: Connection, viaGroup :: Maybe Int64, + chatSettings :: ChatSettings, createdAt :: UTCTime, updatedAt :: UTCTime } @@ -190,6 +191,7 @@ data GroupInfo = GroupInfo localDisplayName :: GroupName, groupProfile :: GroupProfile, membership :: GroupMember, + chatSettings :: ChatSettings, createdAt :: UTCTime, updatedAt :: UTCTime } @@ -200,6 +202,17 @@ instance ToJSON GroupInfo where toEncoding = J.genericToEncoding J.defaultOption groupName' :: GroupInfo -> GroupName groupName' GroupInfo {localDisplayName = g} = g +-- TODO when more settings are added we should create another type to allow partial setting updates (with all Maybe properties) +data ChatSettings = ChatSettings + { enableNtfs :: Bool + } + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON ChatSettings where toEncoding = J.genericToEncoding J.defaultOptions + +defaultChatSettings :: ChatSettings +defaultChatSettings = ChatSettings {enableNtfs = True} + data Profile = Profile { displayName :: ContactName, fullName :: Text, diff --git a/stack.yaml b/stack.yaml index f199a4225f..0bbcb4b8eb 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 3fc9afbb351033413dd84344ec603910e370a8ec + commit: a33539bef9d31f5acbf9a21bfa0ef39a2dd421dd # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7