core: chat items for group preferences (#1402)

* core: chat items for group preferences

* chat items for group preference changes and sent item for contact/user prerences changes

* prohibited features, tests

* enable all tests

* fix
This commit is contained in:
Evgeny Poberezkin
2022-11-23 11:04:08 +00:00
committed by GitHub
parent 67d78e14be
commit e6e5faeb9c
8 changed files with 249 additions and 103 deletions
@@ -12,7 +12,7 @@ import SimpleXChat
struct CIChatFeatureView: View {
var chatItem: ChatItem
var feature: Feature
var enabled: FeatureEnabled?
var iconColor: Color
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
@@ -24,18 +24,11 @@ struct CIChatFeatureView: View {
.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)
CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: .fullDelete, iconColor: enabled.iconColor)
}
}
@@ -31,9 +31,11 @@ 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)
case let .rcvChatFeature(feature, enabled): chatFeatureView(feature, enabled.iconColor)
case let .sndChatFeature(feature, enabled): chatFeatureView(feature, enabled.iconColor)
case let .rcvGroupFeature(feature, preference): chatFeatureView(feature, preference.enable.iconColor)
case let .sndGroupFeature(feature, preference): chatFeatureView(feature, preference.enable.iconColor)
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
}
}
@@ -61,8 +63,8 @@ struct ChatItemView: View {
CIEventView(chatItem: chatItem)
}
private func chatFeatureView(_ feature: Feature, _ enabled: FeatureEnabled?) -> some View {
CIChatFeatureView(chatItem: chatItem, feature: feature, enabled: enabled)
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor)
}
}
+15
View File
@@ -220,6 +220,10 @@ public struct FeatureEnabled: Decodable {
: forContact ? NSLocalizedString("enabled for contact", comment: "enabled status")
: NSLocalizedString("off", comment: "enabled status")
}
public var iconColor: Color {
forUser ? .green : forContact ? .yellow : .secondary
}
}
public enum ContactUserPref: Decodable {
@@ -467,6 +471,13 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable {
case .off: return NSLocalizedString("off", comment: "group pref value")
}
}
public var iconColor: Color {
switch self {
case .on: return .green
case .off: return .secondary
}
}
}
public enum ChatInfo: Identifiable, Decodable, NamedChat {
@@ -1485,6 +1496,8 @@ public enum CIContent: Decodable, ItemContent {
case sndConnEvent(sndConnEvent: SndConnEvent)
case rcvChatFeature(feature: Feature, enabled: FeatureEnabled)
case sndChatFeature(feature: Feature, enabled: FeatureEnabled)
case rcvGroupFeature(feature: Feature, preference: GroupPreference)
case sndGroupFeature(feature: Feature, preference: GroupPreference)
case rcvChatFeatureRejected(feature: Feature)
public var text: String {
@@ -1505,6 +1518,8 @@ public enum CIContent: Decodable, ItemContent {
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 .rcvGroupFeature(feature, preference): return "\(feature.text): \(preference.enable.text)"
case let .sndGroupFeature(feature, preference): return "\(feature.text): \(preference.enable.text)"
case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text)
}
}
+126 -62
View File
@@ -289,16 +289,19 @@ processChatCommand = \case
CTDirect -> do
ct@Contact {localDisplayName = c, contactUsed} <- withStore $ \db -> getContact db user chatId
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
(fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct
(msgContainer, quotedItem_) <- prepareMsg fileInvitation_
(msg@SndMessage {sharedMsgId}, _) <- sendDirectContactMessage ct (XMsgNew msgContainer)
case ft_ of
Just ft@FileTransferMeta {fileInline = Just IFMSent} ->
sendDirectFileInline ct ft sharedMsgId
_ -> pure ()
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_
setActive $ ActiveC c
pure . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci
case featureProhibited forUser user ct mc of
Just f -> pure $ chatCmdError $ "feature not allowed " <> T.unpack (chatFeatureToText f)
_ -> do
(fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct
(msgContainer, quotedItem_) <- prepareMsg fileInvitation_
(msg@SndMessage {sharedMsgId}, _) <- sendDirectContactMessage ct (XMsgNew msgContainer)
case ft_ of
Just ft@FileTransferMeta {fileInline = Just IFMSent} ->
sendDirectFileInline ct ft sharedMsgId
_ -> pure ()
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_
setActive $ ActiveC c
pure . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci
where
setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
setupSndFileTransfer ct = forM file_ $ \file -> do
@@ -335,13 +338,16 @@ processChatCommand = \case
CTGroup -> do
Group gInfo@GroupInfo {membership, localDisplayName = gName} ms <- withStore $ \db -> getGroup db user chatId
unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved
(fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length ms)
(msgContainer, quotedItem_) <- prepareMsg fileInvitation_ membership
msg@SndMessage {sharedMsgId} <- sendGroupMessage gInfo ms (XMsgNew msgContainer)
mapM_ (sendGroupFileInline ms sharedMsgId) ft_
ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_
setActive $ ActiveG gName
pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci
case groupFeatureProhibited gInfo mc of
Just f -> pure $ chatCmdError $ "feature not allowed " <> T.unpack (chatFeatureToText f)
_ -> do
(fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length ms)
(msgContainer, quotedItem_) <- prepareMsg fileInvitation_ membership
msg@SndMessage {sharedMsgId} <- sendGroupMessage gInfo ms (XMsgNew msgContainer)
mapM_ (sendGroupFileInline ms sharedMsgId) ft_
ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_
setActive $ ActiveG gName
pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci
where
setupSndFileTransfer :: GroupInfo -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
setupSndFileTransfer gInfo n = forM file_ $ \file -> do
@@ -988,7 +994,7 @@ processChatCommand = \case
processChatCommand $ APIListMembers groupId
ListGroups -> CRGroupsList <$> withUser (\user -> withStore' (`getUserGroupDetails` user))
APIUpdateGroupProfile groupId p' -> withUser $ \user -> do
Group g ms <- withStore $ \db -> getGroup db user groupId
Group g@GroupInfo {groupProfile = p} ms <- withStore $ \db -> getGroup db user groupId
let s = memberStatus $ membership g
canUpdate =
memberRole (membership g :: GroupMember) == GROwner
@@ -996,8 +1002,11 @@ processChatCommand = \case
unless canUpdate $ throwChatError CEGroupUserRole
g' <- withStore $ \db -> updateGroupProfile db user g p'
msg <- sendGroupMessage g' ms (XGrpInfo p')
ci <- saveSndChatItem user (CDGroupSnd g') msg (CISndGroupEvent $ SGEGroupUpdated p') Nothing Nothing
toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat g') ci
let cd = CDGroupSnd g'
unless (sameGroupProfileInfo p p') $ do
ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') Nothing Nothing
toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat g') ci
createGroupFeatureChangedItems user cd CISndGroupFeature p p'
pure $ CRGroupUpdated g g' Nothing
UpdateGroupProfile gName profile -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName
@@ -1181,6 +1190,7 @@ processChatCommand = \case
forM_ contacts $ \ct -> do
let mergedProfile = userProfileToSend user' Nothing $ Just ct
void (sendDirectContactMessage ct $ XInfo mergedProfile) `catchError` (toView . CRChatError)
createFeatureChangedItems user user' ct ct CDDirectSnd CISndChatFeature
pure $ CRUserProfileUpdated (fromLocalProfile p) p'
updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse
updateContactPrefs user@User {userId} ct@Contact {activeConn = Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs'
@@ -1191,6 +1201,7 @@ processChatCommand = \case
let p' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct')
withChatLock "updateProfile" . procCmd $ do
void (sendDirectContactMessage ct' $ XInfo p') `catchError` (toView . CRChatError)
createFeatureChangedItems user user ct ct' CDDirectSnd CISndChatFeature
pure $ CRContactPrefsUpdated ct ct'
isReady :: Contact -> Bool
isReady ct =
@@ -1782,8 +1793,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
SWITCH qd phase cStats -> do
toView . CRContactSwitch ct $ SwitchProgress qd phase cStats
when (phase /= SPConfirmed) $ case qd of
QDRcv -> createInternalChatItem (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing
QDSnd -> createInternalChatItem (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing
QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
OK ->
-- [async agent commands] continuation on receiving OK
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
@@ -1829,7 +1840,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile groupLinkId
(_msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv
-- we could link chat item with sent group invitation message (_msg)
createInternalChatItem (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
_ -> throwChatError $ CECommandError "unexpected cmdFunction"
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
CONF confId _ connInfo -> do
@@ -1874,8 +1885,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ enableNtfs chatSettings
case memberCategory m of
GCHostMember -> do
memberConnectedChatItem gInfo m
toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected}
createGroupFeatureItems gInfo m
memberConnectedChatItem gInfo m
setActive $ ActiveG gName
showToast ("#" <> gName) "you are connected to group"
GCInviteeMember -> do
@@ -1930,8 +1942,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
SWITCH qd phase cStats -> do
toView . CRGroupMemberSwitch gInfo m $ SwitchProgress qd phase cStats
when (phase /= SPConfirmed) $ case qd of
QDRcv -> createInternalChatItem (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing
QDSnd -> createInternalChatItem (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing
QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
OK ->
-- [async agent commands] continuation on receiving OK
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
@@ -2149,7 +2161,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
memberConnectedChatItem :: GroupInfo -> GroupMember -> m ()
memberConnectedChatItem gInfo m =
-- ts should be broker ts but we don't have it for CON
createInternalChatItem (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing
notifyMemberConnected :: GroupInfo -> GroupMember -> m ()
notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do
@@ -2186,12 +2198,19 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
newContentMessage ct@Contact {localDisplayName = c, contactUsed, chatSettings} mc msg msgMeta = do
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc
ciFile_ <- processFileInvitation fileInvitation_ $ \db -> createRcvFileTransfer db userId ct
ci@ChatItem {formattedText} <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvMsgContent content) ciFile_
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci
when (enableNtfs chatSettings) $ showMsgToast (c <> "> ") content formattedText
let ExtMsgContent content fileInvitation_ = mcExtMsgContent mc
case featureProhibited forContact user ct content of
Just f -> void $ newChatItem (CIRcvChatFeatureRejected f) Nothing
_ -> do
ciFile_ <- processFileInvitation fileInvitation_ $ \db -> createRcvFileTransfer db userId ct
ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) ciFile_
when (enableNtfs chatSettings) $ showMsgToast (c <> "> ") content formattedText
setActive $ ActiveC c
where
newChatItem ciContent ciFile_ = do
ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta ciContent ciFile_
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci
pure ci
processFileInvitation :: Maybe FileInvitation -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv))
processFileInvitation fInv_ createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do
@@ -2245,12 +2264,19 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
newGroupContentMessage gInfo@GroupInfo {chatSettings} m@GroupMember {localDisplayName = c} mc msg msgMeta = do
let (ExtMsgContent content fInv_) = mcExtMsgContent mc
ciFile_ <- processFileInvitation fInv_ $ \db -> createRcvGroupFileTransfer db userId m
ci@ChatItem {formattedText} <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvMsgContent content) ciFile_
groupMsgToView gInfo m ci msgMeta
let g = groupName' gInfo
when (enableNtfs chatSettings) $ showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText
setActive $ ActiveG g
case groupFeatureProhibited gInfo content of
Just f -> void $ newChatItem (CIRcvChatFeatureRejected f) Nothing
_ -> do
ciFile_ <- processFileInvitation fInv_ $ \db -> createRcvGroupFileTransfer db userId m
ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) ciFile_
let g = groupName' gInfo
when (enableNtfs chatSettings) $ showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText
setActive $ ActiveG g
where
newChatItem ciContent ciFile_ = do
ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta ciContent ciFile_
groupMsgToView gInfo m ci msgMeta
pure ci
groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> m ()
groupMessageUpdate gInfo@GroupInfo {groupId, localDisplayName = g} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta =
@@ -2469,38 +2495,28 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
MsgOk -> pure ()
MsgError e -> case e of
MsgSkipped {} -> createInternalChatItem cd (CIRcvIntegrityError e) (Just brokerTs)
MsgSkipped {} -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
_ -> toView $ CRMsgIntegrityError e
createInternalChatItem :: forall c d. (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> CIContent d -> Maybe UTCTime -> m ()
createInternalChatItem cd content itemTs_ = do
createdAt <- liftIO getCurrentTime
let itemTs = fromMaybe createdAt itemTs_
ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content itemTs createdAt
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing itemTs createdAt
toView $ CRNewChatItem $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
xInfo :: Contact -> Profile -> m ()
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
createFeatureChangedItems user user c c' CDDirectRcv CIRcvChatFeature
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
createInternalChatItem user (CDDirectRcv ct) (CIRcvChatFeature f enabled) Nothing
createGroupFeatureItems :: GroupInfo -> GroupMember -> m ()
createGroupFeatureItems g@GroupInfo {groupProfile} m = do
let prefs = mergeGroupPreferences $ groupPreferences groupProfile
forM_ allChatFeatures $ \f -> do
let p = getGroupPreference f prefs
createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature f p) Nothing
xInfoProbe :: Contact -> Probe -> m ()
xInfoProbe c2 probe =
@@ -2789,13 +2805,16 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
toView $ CRGroupDeleted gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m
xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> MsgMeta -> m ()
xGrpInfo g m@GroupMember {memberRole} p' msg msgMeta
xGrpInfo g@GroupInfo {groupProfile = p} m@GroupMember {memberRole} p' msg msgMeta
| memberRole < GROwner = messageError "x.grp.info with insufficient member permissions"
| otherwise = do
| otherwise = unless (p == p') $ do
g' <- withStore $ \db -> updateGroupProfile db user g p'
ci <- saveRcvChatItem user (CDGroupRcv g' m) msg msgMeta (CIRcvGroupEvent $ RGEGroupUpdated p') Nothing
groupMsgToView g' m ci msgMeta
toView . CRGroupUpdated g g' $ Just m
let cd = CDGroupRcv g' m
unless (sameGroupProfileInfo p p') $ do
ci <- saveRcvChatItem user cd msg msgMeta (CIRcvGroupEvent $ RGEGroupUpdated p') Nothing
groupMsgToView g' m ci msgMeta
createGroupFeatureChangedItems user cd CIRcvGroupFeature p p'
sendDirectFileInline :: ChatMonad m => Contact -> FileTransferMeta -> SharedMsgId -> m ()
sendDirectFileInline ct ft sharedMsgId = do
@@ -3068,6 +3087,51 @@ userProfileToSend user@User {profile = p} incognitoProfile ct =
userPrefs = maybe (preferences' user) (const Nothing) incognitoProfile
in (p' :: Profile) {preferences = Just . toChatPrefs $ mergePreferences (userPreferences <$> ct) userPrefs}
createFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> User -> Contact -> Contact -> (Contact -> ChatDirection 'CTDirect d) -> (ChatFeature -> PrefEnabled -> CIContent d) -> m ()
createFeatureChangedItems user user' ct ct' chatDir ciContent =
forM_ allChatFeatures $ \f -> do
let ContactUserPreference {enabled} = getContactUserPreference f cups
ContactUserPreference {enabled = enabled'} = getContactUserPreference f cups'
unless (enabled == enabled') $
createInternalChatItem user (chatDir ct') (ciContent f enabled') Nothing
where
cups = contactUserPreferences' user ct
cups' = contactUserPreferences' user' ct'
createGroupFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> ChatDirection 'CTGroup d -> (ChatFeature -> GroupPreference -> CIContent d) -> GroupProfile -> GroupProfile -> m ()
createGroupFeatureChangedItems user cd ciContent p p' =
forM_ allChatFeatures $ \f -> do
let pref = getGroupPreference f $ groupPreferences p
pref' = getGroupPreference f $ groupPreferences p'
unless (pref == pref') $
createInternalChatItem user cd (ciContent f pref') Nothing
sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool
sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing}
featureProhibited :: (PrefEnabled -> Bool) -> User -> Contact -> MsgContent -> Maybe ChatFeature
featureProhibited forWhom user ct = \case
MCVoice {} ->
let ContactUserPreference {enabled} =
getContactUserPreference CFVoice $ contactUserPreferences' user ct
in if forWhom enabled then Nothing else Just CFVoice
_ -> Nothing
groupFeatureProhibited :: GroupInfo -> MsgContent -> Maybe ChatFeature
groupFeatureProhibited GroupInfo {groupProfile} = \case
MCVoice {} ->
let GroupPreference {enable} = getGroupPreference CFVoice $ groupPreferences groupProfile
in case enable of FEOn -> Nothing; FEOff -> Just CFVoice
_ -> Nothing
createInternalChatItem :: forall c d m. (ChatTypeI c, MsgDirectionI d, ChatMonad m) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m ()
createInternalChatItem user cd content itemTs_ = do
createdAt <- liftIO getCurrentTime
let itemTs = fromMaybe createdAt itemTs_
ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content itemTs createdAt
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing itemTs createdAt
toView $ CRNewChatItem $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
getCreateActiveUser :: SQLiteStore -> IO User
getCreateActiveUser st = do
user <-
+16
View File
@@ -560,6 +560,8 @@ data CIContent (d :: MsgDirection) where
CISndConnEvent :: SndConnEvent -> CIContent 'MDSnd
CIRcvChatFeature :: ChatFeature -> PrefEnabled -> CIContent 'MDRcv
CISndChatFeature :: ChatFeature -> PrefEnabled -> CIContent 'MDSnd
CIRcvGroupFeature :: ChatFeature -> GroupPreference -> CIContent 'MDRcv
CISndGroupFeature :: ChatFeature -> GroupPreference -> 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
@@ -714,6 +716,8 @@ ciContentToText = \case
CISndConnEvent event -> sndConnEventToText event
CIRcvChatFeature feature enabled -> chatFeatureToText feature <> ": " <> prefEnabledToText enabled
CISndChatFeature feature enabled -> chatFeatureToText feature <> ": " <> prefEnabledToText enabled
CIRcvGroupFeature feature pref -> chatFeatureToText feature <> ": " <> groupPrefToText pref
CISndGroupFeature feature pref -> chatFeatureToText feature <> ": " <> groupPrefToText pref
CIRcvChatFeatureRejected feature -> chatFeatureToText feature <> ": received, prohibited"
msgIntegrityError :: MsgErrorType -> Text
@@ -767,6 +771,8 @@ data JSONCIContent
| JCISndConnEvent {sndConnEvent :: SndConnEvent}
| JCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled}
| JCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled}
| JCIRcvGroupFeature {feature :: ChatFeature, preference :: GroupPreference}
| JCISndGroupFeature {feature :: ChatFeature, preference :: GroupPreference}
| JCIRcvChatFeatureRejected {feature :: ChatFeature}
deriving (Generic)
@@ -794,6 +800,8 @@ jsonCIContent = \case
CISndConnEvent sndConnEvent -> JCISndConnEvent {sndConnEvent}
CIRcvChatFeature feature enabled -> JCIRcvChatFeature {feature, enabled}
CISndChatFeature feature enabled -> JCISndChatFeature {feature, enabled}
CIRcvGroupFeature feature preference -> JCIRcvGroupFeature {feature, preference}
CISndGroupFeature feature preference -> JCISndGroupFeature {feature, preference}
CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature}
aciContentJSON :: JSONCIContent -> ACIContent
@@ -813,6 +821,8 @@ aciContentJSON = \case
JCISndConnEvent {sndConnEvent} -> ACIContent SMDSnd $ CISndConnEvent sndConnEvent
JCIRcvChatFeature {feature, enabled} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled
JCISndChatFeature {feature, enabled} -> ACIContent SMDSnd $ CISndChatFeature feature enabled
JCIRcvGroupFeature {feature, preference} -> ACIContent SMDRcv $ CIRcvGroupFeature feature preference
JCISndGroupFeature {feature, preference} -> ACIContent SMDSnd $ CISndGroupFeature feature preference
JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature
-- platform independent
@@ -832,6 +842,8 @@ data DBJSONCIContent
| DBJCISndConnEvent {sndConnEvent :: DBSndConnEvent}
| DBJCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled}
| DBJCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled}
| DBJCIRcvGroupFeature {feature :: ChatFeature, preference :: GroupPreference}
| DBJCISndGroupFeature {feature :: ChatFeature, preference :: GroupPreference}
| DBJCIRcvChatFeatureRejected {feature :: ChatFeature}
deriving (Generic)
@@ -859,6 +871,8 @@ dbJsonCIContent = \case
CISndConnEvent sce -> DBJCISndConnEvent $ SCE sce
CIRcvChatFeature feature enabled -> DBJCIRcvChatFeature {feature, enabled}
CISndChatFeature feature enabled -> DBJCISndChatFeature {feature, enabled}
CIRcvGroupFeature feature preference -> DBJCIRcvGroupFeature {feature, preference}
CISndGroupFeature feature preference -> DBJCISndGroupFeature {feature, preference}
CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature}
aciContentDBJSON :: DBJSONCIContent -> ACIContent
@@ -878,6 +892,8 @@ aciContentDBJSON = \case
DBJCISndConnEvent (SCE sce) -> ACIContent SMDSnd $ CISndConnEvent sce
DBJCIRcvChatFeature {feature, enabled} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled
DBJCISndChatFeature {feature, enabled} -> ACIContent SMDSnd $ CISndChatFeature feature enabled
DBJCIRcvGroupFeature {feature, preference} -> ACIContent SMDRcv $ CIRcvGroupFeature feature preference
DBJCISndGroupFeature {feature, preference} -> ACIContent SMDSnd $ CISndGroupFeature feature preference
DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature
data CICallStatus
+3
View File
@@ -442,6 +442,9 @@ data GroupPreference = GroupPreference
{enable :: GroupFeatureEnabled}
deriving (Eq, Show, Generic, FromJSON)
groupPrefToText :: GroupPreference -> Text
groupPrefToText GroupPreference {enable} = safeDecodeUtf8 $ strEncode enable
instance ToJSON GroupPreference where toEncoding = J.genericToEncoding J.defaultOptions
data FeatureAllowed
+1 -7
View File
@@ -799,16 +799,10 @@ viewGroupUpdated
prefs = mapMaybe viewPref allChatFeatures
viewPref pt
| pref gps == pref gps' = Nothing
| otherwise = Just $ plain (chatPrefName pt) <> " enabled: " <> viewGroupPreference (pref gps')
| otherwise = Just $ plain (chatPrefName pt) <> " enabled: " <> plain (groupPrefToText $ pref gps')
where
pref pss = getGroupPreference pt $ mergeGroupPreferences pss
viewGroupPreference :: GroupPreference -> StyledString
viewGroupPreference = \case
GroupPreference {enable} -> case enable of
FEOn -> "on"
FEOff -> "off"
viewContactAliasUpdated :: Contact -> [StyledString]
viewContactAliasUpdated Contact {localDisplayName = n, profile = LocalProfile {localAlias}}
| localAlias == "" = ["contact " <> ttyContact n <> " alias removed"]
+79 -20
View File
@@ -561,9 +561,9 @@ testGroupShared alice bob cath checkMessages = do
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")])
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")])
cath @@@ [("@bob", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")]
cath #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (0, "connected"), (0, "hello"), (0, "hi there"), (1, "hey team")])
cath #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "connected"), (0, "hello"), (0, "hi there"), (1, "hey team")])
alice #$> ("/_read chat #1 from=1 to=100", id, "ok")
bob #$> ("/_read chat #1 from=1 to=100", id, "ok")
cath #$> ("/_read chat #1 from=1 to=100", id, "ok")
@@ -1262,7 +1262,7 @@ testGroupMessageDelete =
cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))])
-- alice: msg id 5
bob #$> ("/_update item #1 " <> groupItemId 1 6 <> " text hi alice", id, "message updated")
bob #$> ("/_update item #1 " <> groupItemId 2 6 <> " text hi alice", id, "message updated")
concurrently_
(alice <# "#team bob> [edited] hi alice")
( do
@@ -1281,7 +1281,7 @@ testGroupMessageDelete =
(alice <# "#team cath> how are you?")
(bob <# "#team cath> how are you?")
cath #$> ("/_delete item #1 " <> groupItemId 1 6 <> " broadcast", id, "message deleted")
cath #$> ("/_delete item #1 " <> groupItemId 2 6 <> " broadcast", id, "message deleted")
concurrently_
(alice <# "#team cath> [deleted] how are you?")
(bob <# "#team cath> [deleted] how are you?")
@@ -2840,73 +2840,126 @@ testSetConnectionAlias = testChat2 aliceProfile bobProfile $
testSetContactPrefs :: IO ()
testSetContactPrefs = testChat2 aliceProfile bobProfile $
\alice bob -> do
bob ##> "/_profile {\"displayName\": \"bob\", \"fullName\": \"Bob\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
bob <## "profile image removed"
bob <## "updated preferences:"
bob <## "voice messages allowed: no"
(bob </)
connectUsers alice bob
alice ##> "/_set prefs @2 {}"
alice <## "your preferences for bob did not change"
(bob </)
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"always\"}}"
let startFeatures = [(0, "Full deletion: off"), (0, "Voice messages: off")]
alice #$> ("/_get chat @2 count=100", chat, startFeatures)
bob #$> ("/_get chat @2 count=100", chat, startFeatures)
let sendVoice = "/_send @2 json {\"filePath\": \"./tests/fixtures/test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}"
voiceNotAllowed = "bad chat command: feature not allowed Voice messages"
alice ##> sendVoice
alice <## voiceNotAllowed
bob ##> sendVoice
bob <## voiceNotAllowed
alice ##> "/_set prefs @2 {\"voice\": {\"allow\": \"always\"}}"
alice <## "you updated preferences for bob:"
alice <## "full message deletion: enabled for contact (you allow: always, contact allows: no)"
alice <## "voice messages: enabled for contact (you allow: always, contact allows: no)"
alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact")])
bob <## "alice updated preferences for you:"
bob <## "full message deletion: enabled for you (you allow: default (no), contact allows: always)"
bob <## "voice messages: enabled for you (you allow: default (no), contact allows: always)"
bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you")])
alice ##> sendVoice
alice <## voiceNotAllowed
bob ##> sendVoice
bob <# "@alice voice message (00:10)"
bob <# "/f @alice ./tests/fixtures/test.txt"
bob <## "use /fc 1 to cancel sending"
alice <# "bob> voice message (00:10)"
alice <# "bob> sends file test.txt (11 bytes / 11 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it"
(bob </)
alice ##> "/_profile {\"displayName\": \"alice\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"no\"}}}"
alice ##> "/_profile {\"displayName\": \"alice\", \"fullName\": \"\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
alice <## "user full name removed (your contacts are notified)"
alice <## "updated preferences:"
alice <## "voice messages allowed: no"
(alice </)
bob <## "contact alice removed full name"
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"yes\"}}"
alice <## "you updated preferences for bob:"
alice <## "full message deletion: off (you allow: yes, contact allows: no)"
bob <## "alice updated preferences for you:"
bob <## "full message deletion: off (you allow: default (no), contact allows: yes)"
(bob </)
bob ##> "/_profile {\"displayName\": \"bob\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"yes\"}}}"
alice ##> "/_set prefs @2 {\"voice\": {\"allow\": \"yes\"}}"
alice <## "you updated preferences for bob:"
alice <## "voice messages: off (you allow: yes, contact allows: no)"
alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off")])
bob <## "alice updated preferences for you:"
bob <## "voice messages: off (you allow: default (no), contact allows: yes)"
bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off")])
(bob </)
bob ##> "/_profile {\"displayName\": \"bob\", \"fullName\": \"\", \"preferences\": {\"voice\": {\"allow\": \"yes\"}}}"
bob <## "user full name removed (your contacts are notified)"
bob <## "updated preferences:"
bob <## "full message deletion allowed: yes"
bob <## "voice messages allowed: yes"
bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled")])
(bob </)
alice <## "contact bob removed full name"
alice <## "bob updated preferences for you:"
alice <## "full message deletion: enabled (you allow: yes, contact allows: yes)"
alice <## "voice messages: enabled (you allow: yes, contact allows: yes)"
alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "Voice messages: enabled")])
(alice </)
bob ##> "/_set prefs @2 {}"
bob <## "your preferences for alice did not change"
-- no change
bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled")])
(bob </)
(alice </)
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"no\"}}"
alice ##> "/_set prefs @2 {\"voice\": {\"allow\": \"no\"}}"
alice <## "you updated preferences for bob:"
alice <## "full message deletion: off (you allow: no, contact allows: yes)"
alice <## "voice messages: off (you allow: no, contact allows: yes)"
alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "Voice messages: enabled"), (1, "Voice messages: off")])
bob <## "alice updated preferences for you:"
bob <## "full message deletion: off (you allow: default (yes), contact allows: no)"
bob <## "voice messages: off (you allow: default (yes), contact allows: no)"
bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled"), (0, "Voice messages: off")])
testUpdateGroupPrefs :: IO ()
testUpdateGroupPrefs =
testChat2 aliceProfile bobProfile $
\alice bob -> do
createGroup2 "team" alice bob
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected")])
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected")])
threadDelay 1000000
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}}}"
alice <## "updated group preferences:"
alice <## "full message deletion enabled: on"
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on")])
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "full message deletion enabled: on"
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on")])
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}}}"
alice <## "updated group preferences:"
alice <## "full message deletion enabled: off"
alice <## "voice messages enabled: off"
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off")])
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "full message deletion enabled: off"
bob <## "voice messages enabled: off"
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off")])
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}"
alice <## "updated group preferences:"
alice <## "voice messages enabled: on"
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")])
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "voice messages enabled: on"
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on")])
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}"
-- no update
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")])
threadDelay 1000000
alice #> "#team hey"
bob <# "#team alice> hey"
threadDelay 1000000
bob #> "#team hi"
alice <# "#team bob> hi"
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on"), (1, "hey"), (0, "hi")])
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on"), (0, "hey"), (1, "hi")])
testGetSetSMPServers :: IO ()
testGetSetSMPServers =
@@ -3851,7 +3904,7 @@ testSwitchGroupMember =
bob <## "#team: alice changed address for you"
alice <## "#team: you changed address for bob"
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "started changing address for bob..."), (1, "you changed address for bob")])
bob #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (0, "started changing address for you..."), (0, "changed address for you")])
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "changed address for you")])
alice #> "#team hey"
bob <# "#team alice> hey"
bob #> "#team hi"
@@ -4039,6 +4092,12 @@ 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)]
groupFeatures :: [(Int, String)]
groupFeatures = map (\(a, _, _) -> a) groupFeatures''
groupFeatures'' :: [((Int, String), Maybe (Int, String), Maybe String)]
groupFeatures'' = [((0, "Full deletion: off"), Nothing, Nothing), ((0, "Voice messages: on"), Nothing, Nothing)]
itemId :: Int -> String
itemId i = show $ length chatFeatures + i