From c25d0ea224077cf00e453383a546b01d9ea6e759 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 9 Jan 2025 15:58:47 +0000 Subject: [PATCH 1/2] directory: option to run service as CLI (#5494) * directory: option to run service as CLI * support muting groups when joining * fix test --- apps/simplex-directory-service/Main.hs | 6 +- .../src/Directory/Options.hs | 7 ++ .../src/Directory/Service.hs | 73 ++++++++++++------- src/Simplex/Chat/Controller.hs | 4 +- src/Simplex/Chat/Library/Commands.hs | 16 ++-- tests/Bots/DirectoryTests.hs | 1 + 6 files changed, 70 insertions(+), 37 deletions(-) diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs index af9c9dd252..0c6464dbfe 100644 --- a/apps/simplex-directory-service/Main.hs +++ b/apps/simplex-directory-service/Main.hs @@ -10,6 +10,8 @@ import Simplex.Chat.Terminal (terminalChatConfig) main :: IO () main = do - opts@DirectoryOpts {directoryLog} <- welcomeGetOpts + opts@DirectoryOpts {directoryLog, runCLI} <- welcomeGetOpts st <- restoreDirectoryStore directoryLog - simplexChatCore terminalChatConfig (mkChatOpts opts) $ directoryService st opts + if runCLI + then directoryServiceCLI st opts + else simplexChatCore terminalChatConfig (mkChatOpts opts) $ directoryService st opts diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 7f02a580e6..70135e4ccf 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -23,6 +23,7 @@ data DirectoryOpts = DirectoryOpts superUsers :: [KnownContact], directoryLog :: Maybe FilePath, serviceName :: T.Text, + runCLI :: Bool, searchResults :: Int, testing :: Bool } @@ -58,6 +59,11 @@ directoryOpts appDir defaultDbFileName = do <> help "The display name of the directory service bot, without *'s and spaces (SimpleX-Directory)" <> value "SimpleX-Directory" ) + runCLI <- + switch + ( long "run-cli" + <> help "Run directory service as CLI" + ) pure DirectoryOpts { coreOptions, @@ -65,6 +71,7 @@ directoryOpts appDir defaultDbFileName = do superUsers, directoryLog, serviceName = T.pack serviceName, + runCLI, searchResults = 10, testing = False } diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index d2016ff1f5..5b96603f68 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -9,6 +9,7 @@ module Directory.Service ( welcomeGetOpts, directoryService, + directoryServiceCLI, ) where @@ -36,6 +37,8 @@ import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Store.Shared (StoreError (..)) +import Simplex.Chat.Terminal (terminalChatConfig) +import Simplex.Chat.Terminal.Main (simplexChatCLI') import Simplex.Chat.Types import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) @@ -77,33 +80,51 @@ welcomeGetOpts = do putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" pure opts +directoryServiceCLI :: DirectoryStore -> DirectoryOpts -> IO () +directoryServiceCLI st opts = do + env <- newServiceState + eventQ <- newTQueueIO + let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) + race_ + (simplexChatCLI' terminalChatConfig {chatHooks = defaultChatHooks {eventHook}} (mkChatOpts opts) Nothing) + (processEvents eventQ env) + where + processEvents eventQ env = forever $ do + (cc, resp) <- atomically $ readTQueue eventQ + u_ <- readTVarIO (currentUser cc) + forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp + directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO () -directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchResults, testing} user@User {userId} cc = do +directoryService st opts@DirectoryOpts {testing} user cc = do initializeBotAddress' (not testing) cc env <- newServiceState race_ (forever $ void getLine) . forever $ do (_, _, resp) <- atomically . readTBQueue $ outputQ cc - forM_ (crDirectoryEvent resp) $ \case - DEContactConnected ct -> deContactConnected ct - DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole - DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner - DEGroupUpdated {contactId, fromGroup, toGroup} -> deGroupUpdated contactId fromGroup toGroup - DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role - DEServiceRoleChanged g role -> deServiceRoleChanged g role - DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g - DEContactLeftGroup ctId g -> deContactLeftGroup ctId g - DEServiceRemovedFromGroup g -> deServiceRemovedFromGroup g - DEGroupDeleted _g -> pure () - DEUnsupportedMessage _ct _ciId -> pure () - DEItemEditIgnored _ct -> pure () - DEItemDeleteIgnored _ct -> pure () - DEContactCommand ct ciId (ADC sUser cmd) -> do - logInfo $ "command received " <> directoryCmdTag cmd - case sUser of - SDRUser -> deUserCommand env ct ciId cmd - SDRAdmin -> deAdminCommand ct ciId cmd - SDRSuperUser -> deSuperUserCommand ct ciId cmd - DELogChatResponse r -> logInfo r + directoryServiceEvent st opts env user cc resp + +directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> ChatResponse -> IO () +directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, searchResults} ServiceState {searchRequests} user@User {userId} cc event = + forM_ (crDirectoryEvent event) $ \case + DEContactConnected ct -> deContactConnected ct + DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole + DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner + DEGroupUpdated {contactId, fromGroup, toGroup} -> deGroupUpdated contactId fromGroup toGroup + DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role + DEServiceRoleChanged g role -> deServiceRoleChanged g role + DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g + DEContactLeftGroup ctId g -> deContactLeftGroup ctId g + DEServiceRemovedFromGroup g -> deServiceRemovedFromGroup g + DEGroupDeleted _g -> pure () + DEUnsupportedMessage _ct _ciId -> pure () + DEItemEditIgnored _ct -> pure () + DEItemDeleteIgnored _ct -> pure () + DEContactCommand ct ciId (ADC sUser cmd) -> do + logInfo $ "command received " <> directoryCmdTag cmd + case sUser of + SDRUser -> deUserCommand ct ciId cmd + SDRAdmin -> deAdminCommand ct ciId cmd + SDRSuperUser -> deSuperUserCommand ct ciId cmd + DELogChatResponse r -> logInfo r where withAdminUsers action = void . forkIO $ do forM_ superUsers $ \KnownContact {contactId} -> action contactId @@ -153,7 +174,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe processInvitation :: Contact -> GroupInfo -> IO () processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do void $ addGroupReg st ct g GRSProposed - r <- sendChatCmd cc $ APIJoinGroup groupId + r <- sendChatCmd cc $ APIJoinGroup groupId MFNone sendMessage cc ct $ case r of CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" _ -> "Error joining group " <> displayName <> ", please re-send the invitation!" @@ -417,8 +438,8 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." - deUserCommand :: ServiceState -> Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () - deUserCommand env@ServiceState {searchRequests} ct ciId = \case + deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () + deUserCommand ct ciId = \case DCHelp -> sendMessage cc ct $ "You must be the owner to add the group to the directory:\n\ @@ -446,7 +467,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe STRecent -> withFoundListedGroups Nothing $ sendNextSearchResults takeRecent search Nothing -> showAllGroups where - showAllGroups = deUserCommand env ct ciId DCAllGroups + showAllGroups = deUserCommand ct ciId DCAllGroups DCAllGroups -> withFoundListedGroups Nothing $ sendAllGroups takeTop "top" STAll DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent DCSubmitGroup _link -> pure () diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index f6f7416bb1..3854b0662e 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -349,7 +349,7 @@ data ChatCommand | APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString} | ApiGetConnNtfMessages {connIds :: NonEmpty AgentConnId} | APIAddMember GroupId ContactId GroupMemberRole - | APIJoinGroup GroupId + | APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter} | APIMemberRole GroupId GroupMemberId GroupMemberRole | APIBlockMemberForAll GroupId GroupMemberId Bool | APIRemoveMember GroupId GroupMemberId @@ -467,7 +467,7 @@ data ChatCommand | APINewGroup UserId IncognitoEnabled GroupProfile | NewGroup IncognitoEnabled GroupProfile | AddMember GroupName ContactName GroupMemberRole - | JoinGroup GroupName + | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} | MemberRole GroupName ContactName GroupMemberRole | BlockForAll GroupName ContactName Bool | RemoveMember GroupName ContactName diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index d25444a358..85dd765e7a 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1924,12 +1924,12 @@ processChatCommand' vr = \case pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole} Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName - APIJoinGroup groupId -> withUser $ \user@User {userId} -> do + APIJoinGroup groupId enableNtfs -> withUser $ \user@User {userId} -> do withGroupLock "joinGroup" groupId . procCmd $ do (invitation, ct) <- withFastStore $ \db -> do inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId (inv,) <$> getContactViaMember db vr user fromMember - let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation + let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}} = invitation GroupMember {memberId = membershipMemId} = membership Contact {activeConn} = ct case activeConn of @@ -1946,7 +1946,9 @@ processChatCommand' vr = \case withFastStore' $ \db -> do updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted - void (withAgent $ \a -> joinConnection a (aUserId user) agentConnId True connRequest dm PQSupportOff subMode) + -- MFAll is default for new groups + unless (enableNtfs == MFAll) $ updateGroupSettings db user groupId chatSettings {enableNtfs} + void (withAgent $ \a -> joinConnection a (aUserId user) agentConnId (enableNtfs /= MFNone) connRequest dm PQSupportOff subMode) `catchChatError` \e -> do withFastStore' $ \db -> do updateGroupMemberStatus db userId fromMember GSMemInvited @@ -2043,9 +2045,9 @@ processChatCommand' vr = \case AddMember gName cName memRole -> withUser $ \user -> do (groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName processChatCommand $ APIAddMember groupId contactId memRole - JoinGroup gName -> withUser $ \user -> do + JoinGroup gName enableNtfs -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIJoinGroup groupId + processChatCommand $ APIJoinGroup groupId enableNtfs MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMemberRole gId gMemberId memRole BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMemberForAll gId gMemberId blocked RemoveMember gName gMemberName -> withMemberName gName gMemberName APIRemoveMember @@ -3630,7 +3632,7 @@ chatCommandP = "/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP), "/_ntf conn messages " *> (ApiGetConnNtfMessages <$> strP), "/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), - "/_join #" *> (APIJoinGroup <$> A.decimal), + "/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI "/_member role #" *> (APIMemberRole <$> A.decimal <* A.space <*> A.decimal <*> memberRole), "/_block #" *> (APIBlockMemberForAll <$> A.decimal <* A.space <*> A.decimal <* A.space <* "blocked=" <*> onOffP), "/_remove #" *> (APIRemoveMember <$> A.decimal <* A.space <*> A.decimal), @@ -3712,7 +3714,7 @@ chatCommandP = ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile), "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)), - ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName), + ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName <*> (" mute" $> MFNone <|> pure MFAll)), ("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole), "/block for all #" *> (BlockForAll <$> displayName <* A.space <*> (char_ '@' *> displayName) <*> pure True), "/unblock for all #" *> (BlockForAll <$> displayName <* A.space <*> (char_ '@' *> displayName) <*> pure False), diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index c50bb8b02d..9775dddd5f 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -72,6 +72,7 @@ mkDirectoryOpts tmp superUsers = superUsers, directoryLog = Just $ tmp "directory_service.log", serviceName = "SimpleX-Directory", + runCLI = False, searchResults = 3, testing = True } From cd9eb66ebb1e604633cbb05484810227b985af7c Mon Sep 17 00:00:00 2001 From: Diogo Date: Thu, 9 Jan 2025 22:28:29 +0000 Subject: [PATCH 2/2] ui: remove support for inline moderation (#5495) * android: remove support for inline moderation * ios: emove support for inline moderation * fix prefix on preview for ios * unused * final pass * ios: should not be able to assign moderator * button label --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Views/Chat/ChatView.swift | 205 ++------------- .../Chat/Group/GroupMemberInfoView.swift | 32 +-- .../Views/ChatList/ChatPreviewView.swift | 2 +- apps/ios/SimpleXChat/ChatTypes.swift | 13 +- .../chat/simplex/common/model/ChatModel.kt | 13 - .../simplex/common/views/chat/ChatView.kt | 72 +++--- .../views/chat/group/GroupMemberInfoView.kt | 16 +- .../common/views/chat/item/ChatItemView.kt | 238 ++---------------- .../commonMain/resources/MR/base/strings.xml | 9 +- 9 files changed, 106 insertions(+), 494 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index b74cbfbc81..3444fd0723 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -1284,20 +1284,11 @@ struct ChatView: View { @ViewBuilder private func menu(_ ci: ChatItem, _ range: ClosedRange?, live: Bool) -> some View { - if let groupInfo = chat.chatInfo.groupInfo, ci.isReport, ci.meta.itemDeleted == nil { - if ci.chatDir == .groupSnd { - deleteButton(ci) - } else { + if case let .group(gInfo) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil { + if ci.chatDir != .groupSnd, gInfo.membership.memberRole >= .moderator { archiveReportButton(ci) - if let qi = ci.quotedItem { - moderateReportedButton(qi, ci, groupInfo) - if let rMember = qi.memberToModerate(chat.chatInfo) { - if !rMember.blockedByAdmin, rMember.canBlockForAll(groupInfo: groupInfo) { - blockMemberButton(rMember, groupInfo, qi, ci) - } - } - } } + deleteButton(ci, label: "Delete report") } else if let mc = ci.content.msgContent, !ci.isReport, ci.meta.itemDeleted == nil || revealed { if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction, availableReactions.count > 0 { @@ -1351,7 +1342,7 @@ struct ChatView: View { if ci.chatDir != .groupSnd { if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { moderateButton(ci, groupInfo) - } else if ci.meta.itemDeleted == nil, case let .group(gInfo) = chat.chatInfo, gInfo.membership.memberRole < .moderator, !live, composeState.voiceMessageRecordingState == .noRecording { + } else if ci.meta.itemDeleted == nil, case let .group(gInfo) = chat.chatInfo, gInfo.membership.memberRole == .member, !live, composeState.voiceMessageRecordingState == .noRecording { reportButton(ci) } } @@ -1627,7 +1618,7 @@ struct ChatView: View { } } - private func deleteButton(_ ci: ChatItem) -> Button { + private func deleteButton(_ ci: ChatItem, label: LocalizedStringKey = "Delete") -> Button { Button(role: .destructive) { if !revealed, let currIndex = m.getChatItemIndex(ci), @@ -1649,10 +1640,7 @@ struct ChatView: View { deletingItem = ci } } label: { - Label( - NSLocalizedString("Delete", comment: "chat item action"), - systemImage: "trash" - ) + Label(label, systemImage: "trash") } } @@ -1668,31 +1656,19 @@ struct ChatView: View { private func moderateButton(_ ci: ChatItem, _ groupInfo: GroupInfo) -> Button { Button(role: .destructive) { - showModerateMessageAlert(groupInfo) { - deletingItem = ci - deleteMessage(.cidmBroadcast, moderate: true) - } - } label: { - Label( - NSLocalizedString("Moderate", comment: "chat item action"), - systemImage: "flag" - ) - } - } - - private func moderateReportedButton(_ rItem: CIQuote, _ reportItem: ChatItem, _ groupInfo: GroupInfo) -> Button { - Button(role: .destructive) { - showModerateMessageAlert(groupInfo) { - Task { - let deleted = await deleteReportedMessage(rItem, reportItem.id, groupInfo) - if deleted != nil { - await MainActor.run { - deletingItem = reportItem - deleteMessage(.cidmInternalMark, moderate: false) - } - } - } - } + AlertManager.shared.showAlert(Alert( + title: Text("Delete member message?"), + message: Text( + groupInfo.fullGroupPreferences.fullDelete.on + ? "The message will be deleted for all members." + : "The message will be marked as moderated for all members." + ), + primaryButton: .destructive(Text("Delete")) { + deletingItem = ci + deleteMessage(.cidmBroadcast, moderate: true) + }, + secondaryButton: .cancel() + )) } label: { Label( NSLocalizedString("Moderate", comment: "chat item action"), @@ -1715,74 +1691,7 @@ struct ChatView: View { ) ) } label: { - Label( - NSLocalizedString("Archive", comment: "chat item action"), - systemImage: "archivebox" - ) - } - } - - private func blockMemberButton(_ member: GroupMember, _ groupInfo: GroupInfo, _ rItem: CIQuote, _ report: ChatItem) -> Button { - Button(role: .destructive) { - actionSheet = SomeActionSheet( - actionSheet: ActionSheet( - title: Text("Block and moderate?"), - buttons: [ - .destructive(Text("Block and moderate")) { - AlertManager.shared.showAlert( - Alert( - title: Text("Delete member message and block?"), - message: Text( - NSLocalizedString( - groupInfo.fullGroupPreferences.fullDelete.on - ? "The message will be deleted for all members.\nAll new messages from \(member.chatViewName) will be hidden!" - : "The message will be marked as moderated for all members.\n All new messages from \(member.chatViewName) will be hidden!" - , comment: "block and moderate action" - ) - ), - primaryButton: .destructive(Text("Delete and block")) { - Task { - let deleted = await deleteReportedMessage(rItem, report.id, groupInfo) - if deleted != nil { - let blocked = await blockMemberForAll(groupInfo, member, true) - - if blocked != nil { - await MainActor.run { - deletingItem = report - deleteMessage(.cidmInternalMark, moderate: false) - } - } - } - } - }, - secondaryButton: .cancel() - ) - ) - }, - .destructive(Text("Only block")) { - Task { - if (await getLocalIdForReportedMessage(rItem, report.id, groupInfo)) != nil { - AlertManager.shared.showAlert( - blockForAllAlert(groupInfo, member) { - deletingItem = report - deleteMessage(.cidmInternalMark, moderate: false) - } - ) - } else { - showNoMessageMessageAlert() - } - } - }, - .cancel() - ] - ), - id: "blockMember" - ) - } label: { - Label( - NSLocalizedString("Block member", comment: "chat item action"), - systemImage: "hand.raised" - ) + Label("Archive report", systemImage: "archivebox") } } @@ -1886,60 +1795,6 @@ struct ChatView: View { itemIds.forEach { selectedChatItems?.remove($0) } } } - - private func deleteReportedMessage(_ rItem: CIQuote, _ reportId: Int64, _ groupInfo: GroupInfo) async -> ChatItemDeletion? { - do { - let itemId = await getLocalIdForReportedMessage(rItem, reportId, groupInfo) - - if let itemId = itemId { - let deletedItem = try await apiDeleteMemberChatItems( - groupId: groupInfo.apiId, - itemIds: [itemId] - ).first - - if let di = deletedItem { - await MainActor.run { - if let toItem = di.toChatItem { - _ = m.upsertChatItem(chat.chatInfo, toItem.chatItem) - } else { - m.removeChatItem(chat.chatInfo, di.deletedChatItem.chatItem) - } - } - - return di - } - } else { - showNoMessageMessageAlert() - } - } catch { - logger.error("ChatView.deleteReportedMessage error: \(error)") - AlertManager.shared.showAlertMsg(title: LocalizedStringKey("Error"), message: LocalizedStringKey("Failed to delete reported message")) - } - - return nil - } - - private func getLocalIdForReportedMessage(_ rItem: CIQuote, _ reportId: Int64, _ groupInfo: GroupInfo) async -> Int64? { - do { - if let itemId = rItem.itemId { - return itemId - } else { - let reportItem = try await apiGetChatItems( - type: chat.chatInfo.chatType, - id: chat.chatInfo.apiId, - pagination: .around(chatItemId: reportId, count: 0) - ).first - - if let itemId = reportItem?.quotedItem?.itemId { - return itemId - } - } - } catch { - logger.error("ChatView.getLocalIdForReportedMessage error: \(error)") - } - - return nil - } private func deleteMessage(_ mode: CIDeleteMode, moderate: Bool) { logger.debug("ChatView deleteMessage") @@ -2014,26 +1869,6 @@ struct ChatView: View { } } -private func showModerateMessageAlert(_ groupInfo: GroupInfo, _ onModerate: @escaping () -> Void) { - AlertManager.shared.showAlert(Alert( - title: Text("Delete member message?"), - message: Text( - groupInfo.fullGroupPreferences.fullDelete.on - ? "The message will be deleted for all members." - : "The message will be marked as moderated for all members." - ), - primaryButton: .destructive(Text("Delete"), action: onModerate), - secondaryButton: .cancel() - )) -} - -private func showNoMessageMessageAlert() { - AlertManager.shared.showAlertMsg( - title: LocalizedStringKey("No message"), - message: LocalizedStringKey("This message was deleted or not received yet.") - ) -} - private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey { chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 58e22e63a2..78ea394caf 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -764,18 +764,12 @@ func updateMemberSettings(_ gInfo: GroupInfo, _ member: GroupMember, _ memberSet } } -func blockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember, _ onBlocked: (() -> Void)? = nil) -> Alert { +func blockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { Alert( title: Text("Block member for all?"), message: Text("All new messages from \(mem.chatViewName) will be hidden!"), primaryButton: .destructive(Text("Block for all")) { - Task { - let uMember = await blockMemberForAll(gInfo, mem, true) - - if uMember != nil { - onBlocked?() - } - } + blockMemberForAll(gInfo, mem, true) }, secondaryButton: .cancel() ) @@ -786,25 +780,23 @@ func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { title: Text("Unblock member for all?"), message: Text("Messages from \(mem.chatViewName) will be shown!"), primaryButton: .default(Text("Unblock for all")) { - Task { - await blockMemberForAll(gInfo, mem, false) - } + blockMemberForAll(gInfo, mem, false) }, secondaryButton: .cancel() ) } -func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) async -> GroupMember? { - do { - let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked) - await MainActor.run { - _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) +func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) { + Task { + do { + let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked) + await MainActor.run { + _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + } + } catch let error { + logger.error("apiBlockMemberForAll error: \(responseError(error))") } - return updatedMember - } catch let error { - logger.error("apiBlockMemberForAll error: \(responseError(error))") } - return nil } struct GroupMemberInfoView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index a311db7d50..ff5fb2986b 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -277,7 +277,7 @@ struct ChatPreviewView: View { func prefix() -> Text { switch cItem.content.msgContent { - case let .report(text, reason): return Text(!text.isEmpty ? "\(reason.text): " : reason.text).italic().foregroundColor(Color.red) + case let .report(_, reason): return Text(!itemText.isEmpty ? "\(reason.text): " : reason.text).italic().foregroundColor(Color.red) default: return Text("") } } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 2638c56776..f64a1076a5 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2078,7 +2078,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { if !canBeRemoved(groupInfo: groupInfo) { return nil } let userRole = groupInfo.membership.memberRole - return GroupMemberRole.allCases.filter { $0 <= userRole && $0 != .author } + return GroupMemberRole.supportedRoles.filter { $0 <= userRole } } public func canBlockForAll(groupInfo: GroupInfo) -> Bool { @@ -3337,17 +3337,6 @@ public struct CIQuote: Decodable, ItemContent, Hashable { } return CIQuote(chatDir: chatDir, itemId: itemId, sentAt: sentAt, content: mc) } - - public func memberToModerate(_ chatInfo: ChatInfo) -> GroupMember? { - switch (chatInfo, chatDir) { - case let (.group(groupInfo), .groupRcv(groupMember)): - let m = groupInfo.membership - return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole - ? groupMember - : nil - default: return nil - } - } } public struct CIReactionCount: Decodable, Hashable { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 5b05d033c8..96f12b9ce9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2958,19 +2958,6 @@ class CIQuote ( null -> null } - fun memberToModerate(chatInfo: ChatInfo): GroupMember? { - return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) { - val m = chatInfo.groupInfo.membership - if (m.memberRole >= GroupMemberRole.Moderator && m.memberRole >= chatDir.groupMember.memberRole) { - chatDir.groupMember - } else { - null - } - } else { - null - } - } - companion object { fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIDirection?): CIQuote = CIQuote(chatDir = chatDir, itemId = itemId, sentAt = sentAt, content = MsgContent.MCText(text)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index a488b66c8b..64b7cfe9a1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -301,41 +301,41 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - } }, deleteMessage = { itemId, mode -> - val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } - val toModerate = toDeleteItem?.memberToModerate(chatInfo) - val groupInfo = toModerate?.first - val groupMember = toModerate?.second - val deletedChatItem: ChatItem? - val toChatItem: ChatItem? - val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { - chatModel.controller.apiDeleteMemberChatItems( - chatRh, - groupId = groupInfo.groupId, - itemIds = listOf(itemId) - ) - } else { - chatModel.controller.apiDeleteChatItems( - chatRh, - type = chatInfo.chatType, - id = chatInfo.apiId, - itemIds = listOf(itemId), - mode = mode - ) - } - val deleted = r?.firstOrNull() - if (deleted != null) { - deletedChatItem = deleted.deletedChatItem.chatItem - toChatItem = deleted.toChatItem?.chatItem - withChats { - if (toChatItem != null) { - upsertChatItem(chatRh, chatInfo, toChatItem) - } else { - removeChatItem(chatRh, chatInfo, deletedChatItem) + withBGApi { + val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } + val toModerate = toDeleteItem?.memberToModerate(chatInfo) + val groupInfo = toModerate?.first + val groupMember = toModerate?.second + val deletedChatItem: ChatItem? + val toChatItem: ChatItem? + val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { + chatModel.controller.apiDeleteMemberChatItems( + chatRh, + groupId = groupInfo.groupId, + itemIds = listOf(itemId) + ) + } else { + chatModel.controller.apiDeleteChatItems( + chatRh, + type = chatInfo.chatType, + id = chatInfo.apiId, + itemIds = listOf(itemId), + mode = mode + ) + } + val deleted = r?.firstOrNull() + if (deleted != null) { + deletedChatItem = deleted.deletedChatItem.chatItem + toChatItem = deleted.toChatItem?.chatItem + withChats { + if (toChatItem != null) { + upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + removeChatItem(chatRh, chatInfo, deletedChatItem) + } } } } - - deleted }, deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) }, receiveFile = { fileId -> @@ -599,7 +599,7 @@ fun ChatLayout( info: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, - deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?, + deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, @@ -946,7 +946,7 @@ fun BoxScope.ChatItemsList( showMemberInfo: (GroupInfo, GroupMember) -> Unit, showChatInfo: () -> Unit, loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, - deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?, + deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, @@ -2438,7 +2438,7 @@ fun PreviewChatLayout() { info = {}, showMemberInfo = { _, _ -> }, loadMessages = { _, _, _, _ -> }, - deleteMessage = { _, _ -> null }, + deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, receiveFile = { _ -> }, cancelFile = {}, @@ -2511,7 +2511,7 @@ fun PreviewGroupChatLayout() { info = {}, showMemberInfo = { _, _ -> }, loadMessages = { _, _, _, _ -> }, - deleteMessage = { _, _ -> null }, + deleteMessage = { _, _ -> }, deleteMessages = {}, receiveFile = { _ -> }, cancelFile = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index a064058533..760f340851 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -747,13 +747,13 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem } } -fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember, blockMember: () -> Unit = { withBGApi { blockMemberForAll(rhId, gInfo, mem, true) } }) { +fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.block_for_all_question), text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName), confirmText = generalGetString(MR.strings.block_for_all), onConfirm = { - blockMember() + blockMemberForAll(rhId, gInfo, mem, true) }, destructive = true, ) @@ -765,15 +765,17 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName), confirmText = generalGetString(MR.strings.unblock_for_all), onConfirm = { - withBGApi { blockMemberForAll(rhId, gInfo, mem, false) } + blockMemberForAll(rhId, gInfo, mem, false) }, ) } -suspend fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { - val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) - withChats { - upsertGroupMember(rhId, gInfo, updatedMember) +fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { + withBGApi { + val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) + withChats { + upsertGroupMember(rhId, gInfo, updatedMember) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 1a094f613a..58e4a31840 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -27,12 +27,9 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.currentUser -import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* -import chat.simplex.common.views.chat.group.blockForAllAlert -import chat.simplex.common.views.chat.group.blockMemberForAll import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.datetime.Clock @@ -77,7 +74,7 @@ fun ChatItemView( selectedChatItems: MutableState?>, fillMaxWidth: Boolean = true, selectChatItem: () -> Unit, - deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?, + deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, @@ -113,12 +110,6 @@ fun ChatItemView( val onLinkLongClick = { _: String -> showMenu.value = true } val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value - val deleteMessageAsync: (Long, CIDeleteMode) -> Unit = { id, mode -> - withBGApi { - deleteMessage(id, mode) - } - } - Box( modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier, contentAlignment = alignment, @@ -293,7 +284,7 @@ fun ChatItemView( @Composable fun DeleteItemMenu() { DefaultDropdownMenu(showMenu) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -308,31 +299,12 @@ fun ChatItemView( // cItem.id check is a special case for live message chat item which has negative ID while not sent yet cItem.isReport && cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group -> { DefaultDropdownMenu(showMenu) { - if (cItem.chatDir is CIDirection.GroupSnd) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) - } else { - ArchiveReportItemAction(cItem, showMenu, deleteMessageAsync) - val qItem = cItem.quotedItem - if (qItem != null) { - ModerateReportItemAction(rhId, cInfo, cItem, qItem, showMenu, deleteMessage) - val rMember = qItem.memberToModerate(cInfo) - if (rMember != null && !rMember.blockedByAdmin && rMember.canBlockForAll(cInfo.groupInfo)) { - BlockMemberAction( - rhId, - chatInfo = cInfo, - groupInfo = cInfo.groupInfo, - cItem = cItem, - reportedItem = qItem, - member = rMember, - showMenu = showMenu, - deleteMessage = deleteMessage - ) - } - } - - Divider() - SelectItemAction(showMenu, selectChatItem) + if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ArchiveReportItemAction(cItem, showMenu, deleteMessage) } + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report)) + Divider() + SelectItemAction(showMenu, selectChatItem) } } cItem.content.msgContent != null && cItem.id >= 0 && !cItem.isReport -> { @@ -421,13 +393,13 @@ fun ChatItemView( CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) } if (!(live && cItem.meta.isLive) && !preview) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) } if (cItem.chatDir !is CIDirection.GroupSnd) { val groupInfo = cItem.memberToModerate(cInfo)?.first if (groupInfo != null) { - ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessageAsync) - } else if (cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group && cInfo.groupInfo.membership.memberRole < GroupMemberRole.Moderator && !live) { + ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessage) + } else if (cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group && cInfo.groupInfo.membership.memberRole == GroupMemberRole.Member && !live) { ReportItemAction(cItem, composeState, showMenu) } } @@ -447,7 +419,7 @@ fun ChatItemView( ExpandItemAction(revealed, showMenu, reveal) } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -457,7 +429,7 @@ fun ChatItemView( cItem.isDeletedContent -> { DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -471,7 +443,7 @@ fun ChatItemView( } else { ExpandItemAction(revealed, showMenu, reveal) } - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -480,7 +452,7 @@ fun ChatItemView( } else -> { DefaultDropdownMenu(showMenu) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (selectedChatItems.value == null) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -497,7 +469,7 @@ fun ChatItemView( RevealItemAction(revealed, showMenu, reveal) } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -531,7 +503,7 @@ fun ChatItemView( DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -588,7 +560,7 @@ fun ChatItemView( MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessageAsync, deleteMessages) + DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { Divider() SelectItemAction(showMenu, selectChatItem) @@ -772,9 +744,10 @@ fun DeleteItemAction( questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, + buttonText: String = stringResource(MR.strings.delete_verb), ) { ItemAction( - stringResource(MR.strings.delete_verb), + buttonText, painterResource(MR.images.ic_delete), onClick = { showMenu.value = false @@ -822,7 +795,7 @@ fun ModerateItemAction( painterResource(MR.images.ic_flag), onClick = { showMenu.value = false - moderateMessageAlertDialog(cItem.id, questionText, deleteMessage = deleteMessage) + moderateMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage) }, color = Color.Red ) @@ -937,120 +910,10 @@ private fun ReportItemAction( ) } -@Composable -private fun ModerateReportItemAction( - rhId: Long?, - chatInfo: ChatInfo, - cItem: ChatItem, - reportedItem: CIQuote, - showMenu: MutableState, - deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion? -) { - ItemAction( - stringResource(MR.strings.moderate_verb), - painterResource(MR.images.ic_flag), - onClick = { - withBGApi { - val reportedMessageId = getLocalIdForReportedMessage(rhId, chatInfo, reportedItem, cItem.id) - if (reportedMessageId != null) { - moderateMessageAlertDialog( - reportedMessageId, - questionText = moderateMessageQuestionText(chatInfo.featureEnabled(ChatFeature.FullDelete), 1), - deleteMessage = { id, m -> - withApi { - val deleted = deleteMessage(id, m) - if (deleted != null) { - deleteMessage(cItem.id, CIDeleteMode.cidmInternalMark) - } - } - }, - ) - } - } - showMenu.value = false - }, - color = Color.Red - ) -} - -@Composable -private fun BlockMemberAction( - rhId: Long?, - chatInfo: ChatInfo, - groupInfo: GroupInfo, - cItem: ChatItem, - reportedItem: CIQuote, - member: GroupMember, - showMenu: MutableState, - deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion? -) { - ItemAction( - stringResource(MR.strings.block_member_button), - painterResource(MR.images.ic_back_hand), - onClick = { - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.report_block_and_moderate_title), - buttons = { - SectionItemView({ - AlertManager.shared.hideAlert() - withBGApi { - val reportedMessageId = getLocalIdForReportedMessage(rhId, chatInfo, reportedItem, cItem.id) - if (reportedMessageId != null) { - blockAndModerateAlertDialog( - rhId, - reportedMessageId = reportedMessageId, - reportId = cItem.id, - gInfo = groupInfo, - mem = member, - deleteMessage = deleteMessage, - ) - } - } - }) { - Text(generalGetString(MR.strings.report_block_and_moderate_block_and_moderate_action), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - SectionItemView({ - AlertManager.shared.hideAlert() - withBGApi { - val reportedMessageId = getLocalIdForReportedMessage(rhId, chatInfo, reportedItem, cItem.id) - if (reportedMessageId != null) { - blockForAllAlert(rhId, gInfo = groupInfo, mem = member, blockMember = { - withBGApi { - try { - blockMemberForAll( - rhId, - gInfo = groupInfo, - member = member, - blocked = true - ) - deleteMessage(reportedMessageId, CIDeleteMode.cidmInternalMark) - } catch (ex: Exception) { - Log.e(TAG, "BlockMemberAction block and moderate ${ex.message}") - } - } - }) - } - } - }) { - Text(generalGetString(MR.strings.report_block_and_moderate_only_block_action), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - SectionItemView({ - AlertManager.shared.hideAlert() - }) { - Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - ) - showMenu.value = false - }, - color = Color.Red - ) -} - @Composable private fun ArchiveReportItemAction(cItem: ChatItem, showMenu: MutableState, deleteMessage: (Long, CIDeleteMode) -> Unit) { ItemAction( - stringResource(MR.strings.archive_verb), + stringResource(MR.strings.archive_report), painterResource(MR.images.ic_inventory_2), onClick = { AlertManager.shared.showAlertDialog( @@ -1401,14 +1264,14 @@ fun moderateMessageQuestionText(fullDeleteAllowed: Boolean, count: Int): String } } -fun moderateMessageAlertDialog(chatItemId: Long, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) { +fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_member_message__question), text = questionText, confirmText = generalGetString(MR.strings.delete_verb), destructive = true, onConfirm = { - deleteMessage(chatItemId, CIDeleteMode.cidmBroadcast) + deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast) } ) } @@ -1423,59 +1286,8 @@ fun moderateMessagesAlertDialog(itemIds: List, questionText: String, delet ) } -private fun blockAndModerateAlertDialog( - rhId: Long?, - reportedMessageId: Long, - reportId: Long, - gInfo: GroupInfo, - mem: GroupMember, - deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion? -) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.report_block_and_moderate_confirmation_title), - text = generalGetString( - if (gInfo.fullGroupPreferences.fullDelete.on) MR.strings.report_block_and_moderate_confirmation_desc_full_delete else MR.strings.report_block_and_moderate_confirmation_desc_full_delete).format(mem.chatViewName), - confirmText = generalGetString(MR.strings.report_block_and_moderate_confirmation_ok), - onConfirm = { - withBGApi { - try { - val deleted = deleteMessage(reportedMessageId, CIDeleteMode.cidmBroadcast) - if (deleted != null) { - blockMemberForAll(rhId, gInfo, mem, true) - deleteMessage(reportId, CIDeleteMode.cidmInternalMark) - } - } catch (ex: Exception) { - Log.e(TAG, "blockAndModerateAlertDialog block and moderate ${ex.message}") - } - } - }, - destructive = true, - ) -} - expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) -private suspend fun getLocalIdForReportedMessage( - rhId: Long?, - chatInfo: ChatInfo, - reportedItem: CIQuote, - itemId: Long): Long? { - if (reportedItem.itemId != null) { - return reportedItem.itemId - } - val item = apiLoadSingleMessage(rhId, chatInfo.chatType, chatInfo.apiId, itemId) - - if (item?.quotedItem?.itemId != null) { - withChats { - updateChatItem(chatInfo, item) - } - return item.quotedItem.itemId - } else { - showQuotedItemDoesNotExistAlert() - return null - } -} - @Preview @Composable fun PreviewChatItemView( @@ -1493,7 +1305,7 @@ fun PreviewChatItemView( range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, selectChatItem = {}, - deleteMessage = { _, _ -> null }, + deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, receiveFile = { _ -> }, cancelFile = {}, @@ -1539,7 +1351,7 @@ fun PreviewChatItemViewDeletedContent() { range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, selectChatItem = {}, - deleteMessage = { _, _ -> null }, + deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, receiveFile = { _ -> }, cancelFile = {}, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 860975a414..c9c0f00555 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -305,13 +305,6 @@ Report reason? Archive report? The report will be archived for you. - Block and moderate? - Block and moderate - Only block - Delete member message and block? - The message will be deleted for all members.\nAll new messages from %1$s will be hidden! - The message will be marked as moderated for all members.\nAll new messages from %1$s will be hidden! - Delete and block Error: %1$s @@ -338,6 +331,8 @@ Info Search Archive + Archive report + Delete report Sent message Received message History