mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-24 23:55:50 +00:00
ui: archive multiple reports (#5619)
* android, desktop: archive multiple reports * ios * change * changes * fix changing counter * fix changing counter2 * fix changing counter3 * unused * fix android * android notification * simplify * ios notification * orange * orange * core: update api * buttons * ios api * android api * fix 4 buttons * buttons and check for member active status * android colors and member active * show delete group button when not in the group anymore * title --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
committed by
GitHub
parent
c5bb2c4ca2
commit
e7361cf025
@@ -918,7 +918,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func decreaseGroupReportsCounter(_ chatId: ChatId, by: Int = 1) {
|
||||
changeGroupReportsCounter(chatId, -1)
|
||||
changeGroupReportsCounter(chatId, -by)
|
||||
}
|
||||
|
||||
private func changeGroupReportsCounter(_ chatId: ChatId, _ by: Int = 0) {
|
||||
|
||||
@@ -516,6 +516,18 @@ func apiDeleteMemberChatItems(groupId: Int64, itemIds: [Int64]) async throws ->
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiArchiveReceivedReports(groupId: Int64) async throws -> ChatResponse {
|
||||
let r = await chatSendCmd(.apiArchiveReceivedReports(groupId: groupId), bgDelay: msgDelay)
|
||||
if case .groupChatItemsDeleted = r { return r }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
|
||||
let r = await chatSendCmd(.apiDeleteReceivedReports(groupId: groupId, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
|
||||
if case let .chatItemsDeleted(_, chatItemDeletions, _) = r { return chatItemDeletions }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String?) {
|
||||
let r = chatSendCmdSync(.apiGetNtfToken)
|
||||
switch r {
|
||||
@@ -2134,42 +2146,13 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
} else {
|
||||
m.removeChatItem(item.deletedChatItem.chatInfo, item.deletedChatItem.chatItem)
|
||||
}
|
||||
if item.deletedChatItem.chatItem.isActiveReport {
|
||||
m.decreaseGroupReportsCounter(item.deletedChatItem.chatInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .groupChatItemsDeleted(user, groupInfo, chatItemIDs, _, member_):
|
||||
if !active(user) {
|
||||
do {
|
||||
let users = try listUsers()
|
||||
await MainActor.run {
|
||||
m.users = users
|
||||
}
|
||||
} catch {
|
||||
logger.error("Error loading users: \(error)")
|
||||
}
|
||||
return
|
||||
}
|
||||
let im = ItemsModel.shared
|
||||
let cInfo = ChatInfo.group(groupInfo: groupInfo)
|
||||
await MainActor.run {
|
||||
m.decreaseGroupReportsCounter(cInfo.id, by: chatItemIDs.count)
|
||||
}
|
||||
var notFound = chatItemIDs.count
|
||||
for ci in im.reversedChatItems {
|
||||
if chatItemIDs.contains(ci.id) {
|
||||
let deleted = if case let .groupRcv(groupMember) = ci.chatDir, let member_, groupMember.groupMemberId != member_.groupMemberId {
|
||||
CIDeleted.moderated(deletedTs: Date.now, byGroupMember: member_)
|
||||
} else {
|
||||
CIDeleted.deleted(deletedTs: Date.now)
|
||||
}
|
||||
await MainActor.run {
|
||||
var newItem = ci
|
||||
newItem.meta.itemDeleted = deleted
|
||||
_ = m.upsertChatItem(cInfo, newItem)
|
||||
}
|
||||
notFound -= 1
|
||||
if notFound == 0 { break }
|
||||
}
|
||||
}
|
||||
await groupChatItemsDeleted(user, groupInfo, chatItemIDs, member_)
|
||||
case let .receivedGroupInvitation(user, groupInfo, _, _):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
@@ -2512,6 +2495,43 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
|
||||
}
|
||||
}
|
||||
|
||||
func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemIDs: Set<Int64>, _ member_: GroupMember?) async {
|
||||
let m = ChatModel.shared
|
||||
if !active(user) {
|
||||
do {
|
||||
let users = try listUsers()
|
||||
await MainActor.run {
|
||||
m.users = users
|
||||
}
|
||||
} catch {
|
||||
logger.error("Error loading users: \(error)")
|
||||
}
|
||||
return
|
||||
}
|
||||
let im = ItemsModel.shared
|
||||
let cInfo = ChatInfo.group(groupInfo: groupInfo)
|
||||
await MainActor.run {
|
||||
m.decreaseGroupReportsCounter(cInfo.id, by: chatItemIDs.count)
|
||||
}
|
||||
var notFound = chatItemIDs.count
|
||||
for ci in im.reversedChatItems {
|
||||
if chatItemIDs.contains(ci.id) {
|
||||
let deleted = if case let .groupRcv(groupMember) = ci.chatDir, let member_, groupMember.groupMemberId != member_.groupMemberId {
|
||||
CIDeleted.moderated(deletedTs: Date.now, byGroupMember: member_)
|
||||
} else {
|
||||
CIDeleted.deleted(deletedTs: Date.now)
|
||||
}
|
||||
await MainActor.run {
|
||||
var newItem = ci
|
||||
newItem.meta.itemDeleted = deleted
|
||||
_ = m.upsertChatItem(cInfo, newItem)
|
||||
}
|
||||
notFound -= 1
|
||||
if notFound == 0 { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshCallInvitations() async throws {
|
||||
let m = ChatModel.shared
|
||||
let callInvitations = try await apiGetCallInvitations()
|
||||
|
||||
@@ -46,6 +46,7 @@ struct ChatView: View {
|
||||
@State private var forwardedChatItems: [ChatItem] = []
|
||||
@State private var selectedChatItems: Set<Int64>? = nil
|
||||
@State private var showDeleteSelectedMessages: Bool = false
|
||||
@State private var showArchiveSelectedReports: Bool = false
|
||||
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
|
||||
|
||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||
@@ -100,6 +101,9 @@ struct ChatView: View {
|
||||
allowToDeleteSelectedMessagesForAll = forAll
|
||||
showDeleteSelectedMessages = true
|
||||
},
|
||||
archiveItems: {
|
||||
showArchiveSelectedReports = true
|
||||
},
|
||||
moderateItems: {
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
showModerateSelectedMessagesAlert(groupInfo)
|
||||
@@ -135,6 +139,20 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog(selectedChatItems?.count == 1 ? "Archive report?" : "Archive \((selectedChatItems?.count ?? 0)) reports?", isPresented: $showArchiveSelectedReports, titleVisibility: .visible) {
|
||||
Button("For me", role: .destructive) {
|
||||
if let selected = selectedChatItems {
|
||||
archiveReports(chat.chatInfo, selected.sorted(), false, deletedSelectedMessages)
|
||||
}
|
||||
}
|
||||
if case let ChatInfo.group(groupInfo) = chat.chatInfo, groupInfo.membership.memberActive {
|
||||
Button("For all moderators", role: .destructive) {
|
||||
if let selected = selectedChatItems {
|
||||
archiveReports(chat.chatInfo, selected.sorted(), true, deletedSelectedMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
Group {
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
@@ -914,6 +932,8 @@ struct ChatView: View {
|
||||
@State private var showDeleteMessage = false
|
||||
@State private var deletingItems: [Int64] = []
|
||||
@State private var showDeleteMessages = false
|
||||
@State private var archivingReports: Set<Int64>? = nil
|
||||
@State private var showArchivingReports = false
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var msgWidth: CGFloat = 0
|
||||
@@ -1233,6 +1253,22 @@ struct ChatView: View {
|
||||
deleteMessages(chat, deletingItems, moderate: false)
|
||||
}
|
||||
}
|
||||
.confirmationDialog(archivingReports?.count == 1 ? "Archive report?" : "Archive \(archivingReports?.count ?? 0) reports?", isPresented: $showArchivingReports, titleVisibility: .visible) {
|
||||
Button("For me", role: .destructive) {
|
||||
if let reports = self.archivingReports {
|
||||
archiveReports(chat.chatInfo, reports.sorted(), false)
|
||||
self.archivingReports = []
|
||||
}
|
||||
}
|
||||
if case let ChatInfo.group(groupInfo) = chat.chatInfo, groupInfo.membership.memberActive {
|
||||
Button("For all moderators", role: .destructive) {
|
||||
if let reports = self.archivingReports {
|
||||
archiveReports(chat.chatInfo, reports.sorted(), true)
|
||||
self.archivingReports = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
|
||||
.sheet(isPresented: $showChatItemInfoSheet, onDismiss: {
|
||||
@@ -1694,18 +1730,9 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
private func archiveReportButton(_ cItem: ChatItem) -> Button<some View> {
|
||||
Button(role: .destructive) {
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text("Archive report?"),
|
||||
message: Text("The report will be archived for you."),
|
||||
primaryButton: .destructive(Text("Archive")) {
|
||||
deletingItem = cItem
|
||||
deleteMessage(.cidmInternalMark, moderate: false)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
)
|
||||
Button {
|
||||
archivingReports = [cItem.id]
|
||||
showArchivingReports = true
|
||||
} label: {
|
||||
Label("Archive report", systemImage: "archivebox")
|
||||
}
|
||||
@@ -1936,6 +1963,37 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe
|
||||
}
|
||||
}
|
||||
|
||||
func archiveReports(_ chatInfo: ChatInfo, _ itemIds: [Int64], _ forAll: Bool, _ onSuccess: @escaping () async -> Void = {}) {
|
||||
if itemIds.count > 0 {
|
||||
Task {
|
||||
do {
|
||||
let deleted = try await apiDeleteReceivedReports(
|
||||
groupId: chatInfo.apiId,
|
||||
itemIds: itemIds,
|
||||
mode: forAll ? CIDeleteMode.cidmBroadcast : CIDeleteMode.cidmInternalMark
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
for di in deleted {
|
||||
if let toItem = di.toChatItem {
|
||||
_ = ChatModel.shared.upsertChatItem(chatInfo, toItem.chatItem)
|
||||
} else {
|
||||
ChatModel.shared.removeChatItem(chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
let deletedItem = di.deletedChatItem.chatItem
|
||||
if deletedItem.isActiveReport {
|
||||
ChatModel.shared.decreaseGroupReportsCounter(chatInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
await onSuccess()
|
||||
} catch {
|
||||
logger.error("ChatView.archiveReports error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func buildTheme() -> AppTheme {
|
||||
if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) {
|
||||
let perChatTheme = if case let .direct(contact) = chat.chatInfo {
|
||||
|
||||
@@ -30,12 +30,15 @@ struct SelectedItemsBottomToolbar: View {
|
||||
var chatInfo: ChatInfo
|
||||
// Bool - delete for everyone is possible
|
||||
var deleteItems: (Bool) -> Void
|
||||
var archiveItems: () -> Void
|
||||
var moderateItems: () -> Void
|
||||
//var shareItems: () -> Void
|
||||
var forwardItems: () -> Void
|
||||
@State var deleteEnabled: Bool = false
|
||||
@State var deleteForEveryoneEnabled: Bool = false
|
||||
|
||||
@State var canArchiveReports: Bool = false
|
||||
|
||||
@State var canModerate: Bool = false
|
||||
@State var moderateEnabled: Bool = false
|
||||
|
||||
@@ -50,7 +53,11 @@ struct SelectedItemsBottomToolbar: View {
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Button {
|
||||
deleteItems(deleteForEveryoneEnabled)
|
||||
if canArchiveReports {
|
||||
archiveItems()
|
||||
} else {
|
||||
deleteItems(deleteForEveryoneEnabled)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.resizable()
|
||||
@@ -109,19 +116,25 @@ struct SelectedItemsBottomToolbar: View {
|
||||
deleteCountProhibited = count == 0 || count > 200
|
||||
forwardCountProhibited = count == 0 || count > 20
|
||||
canModerate = possibleToModerate(chatInfo)
|
||||
let groupInfo: GroupInfo? = if case let ChatInfo.group(groupInfo: info) = chatInfo {
|
||||
info
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
if let selected = selectedItems {
|
||||
let me: Bool
|
||||
let onlyOwnGroupItems: Bool
|
||||
(deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, forwardEnabled, selectedChatItems) = chatItems.reduce((true, true, true, true, true, [])) { (r, ci) in
|
||||
(deleteEnabled, deleteForEveryoneEnabled, canArchiveReports, me, onlyOwnGroupItems, forwardEnabled, selectedChatItems) = chatItems.reduce((true, true, true, true, true, true, [])) { (r, ci) in
|
||||
if selected.contains(ci.id) {
|
||||
var (de, dee, me, onlyOwnGroupItems, fe, sel) = r
|
||||
var (de, dee, ar, me, onlyOwnGroupItems, fe, sel) = r
|
||||
de = de && ci.canBeDeletedForSelf
|
||||
dee = dee && ci.meta.deletable && !ci.localNote && !ci.isReport
|
||||
ar = ar && ci.isActiveReport && ci.chatDir != .groupSnd && groupInfo != nil && groupInfo!.membership.memberRole >= .moderator
|
||||
onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd && !ci.isReport
|
||||
me = me && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil && !ci.isReport
|
||||
fe = fe && ci.content.msgContent != nil && ci.meta.itemDeleted == nil && !ci.isLiveDummy && !ci.isReport
|
||||
sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list
|
||||
return (de, dee, me, onlyOwnGroupItems, fe, sel)
|
||||
return (de, dee, ar, me, onlyOwnGroupItems, fe, sel)
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -211,23 +211,32 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
tagChatButton(chat)
|
||||
let showReportsButton = chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= .moderator
|
||||
let showClearButton = !chat.chatItems.isEmpty
|
||||
let showDeleteGroup = groupInfo.canDelete
|
||||
let showLeaveGroup = groupInfo.membership.memberCurrent
|
||||
let totalNumberOfButtons = 1 + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0)
|
||||
let totalNumberOfButtons = 1 + (showReportsButton ? 1 : 0) + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0)
|
||||
|
||||
if showClearButton, totalNumberOfButtons <= 3 {
|
||||
if showClearButton && totalNumberOfButtons <= 3 {
|
||||
clearChatButton()
|
||||
}
|
||||
if (showLeaveGroup) {
|
||||
|
||||
if showReportsButton && totalNumberOfButtons <= 3 {
|
||||
archiveAllReportsButton()
|
||||
}
|
||||
|
||||
if showLeaveGroup {
|
||||
leaveGroupChatButton(groupInfo)
|
||||
}
|
||||
|
||||
if showDeleteGroup {
|
||||
if totalNumberOfButtons <= 3 {
|
||||
|
||||
if showDeleteGroup && totalNumberOfButtons <= 3 {
|
||||
deleteGroupChatButton(groupInfo)
|
||||
} else if totalNumberOfButtons > 3 {
|
||||
if showDeleteGroup && !groupInfo.membership.memberActive {
|
||||
deleteGroupChatButton(groupInfo)
|
||||
moreOptionsButton(false, chat, groupInfo)
|
||||
} else {
|
||||
moreOptionsButton(chat, groupInfo)
|
||||
moreOptionsButton(true, chat, groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,6 +322,14 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func archiveAllReportsButton() -> some View {
|
||||
Button {
|
||||
AlertManager.shared.showAlert(archiveAllReportsAlert())
|
||||
} label: {
|
||||
SwipeLabel(NSLocalizedString("Archive reports", comment: "swipe action"), systemImage: "archivebox", inverted: oneHandUI)
|
||||
}
|
||||
}
|
||||
|
||||
private func clearChatButton() -> some View {
|
||||
Button {
|
||||
AlertManager.shared.showAlert(clearChatAlert())
|
||||
@@ -354,15 +371,20 @@ struct ChatListNavLink: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func moreOptionsButton(_ chat: Chat, _ groupInfo: GroupInfo?) -> some View {
|
||||
private func moreOptionsButton(_ canShowGroupDelete: Bool, _ chat: Chat, _ groupInfo: GroupInfo?) -> some View {
|
||||
Button {
|
||||
var buttons: [Alert.Button] = [
|
||||
.default(Text("Clear")) {
|
||||
AlertManager.shared.showAlert(clearChatAlert())
|
||||
}
|
||||
]
|
||||
|
||||
if let gi = groupInfo, gi.canDelete {
|
||||
var buttons: [Alert.Button] = []
|
||||
buttons.append(.default(Text("Clear")) {
|
||||
AlertManager.shared.showAlert(clearChatAlert())
|
||||
})
|
||||
|
||||
if let groupInfo, chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= .moderator && groupInfo.ready {
|
||||
buttons.append(.default(Text("Archive reports")) {
|
||||
AlertManager.shared.showAlert(archiveAllReportsAlert())
|
||||
})
|
||||
}
|
||||
|
||||
if canShowGroupDelete, let gi = groupInfo, gi.canDelete {
|
||||
buttons.append(.destructive(Text("Delete")) {
|
||||
AlertManager.shared.showAlert(deleteGroupAlert(gi))
|
||||
})
|
||||
@@ -372,7 +394,7 @@ struct ChatListNavLink: View {
|
||||
|
||||
actionSheet = SomeActionSheet(
|
||||
actionSheet: ActionSheet(
|
||||
title: Text("Clear or delete group?"),
|
||||
title: canShowGroupDelete ? Text("Clear or delete group?") : Text("Clear group?"),
|
||||
buttons: buttons
|
||||
),
|
||||
id: "other options"
|
||||
@@ -490,6 +512,27 @@ struct ChatListNavLink: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func archiveAllReportsAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Archive all reports?"),
|
||||
message: Text("All reports will be archived for you."),
|
||||
primaryButton: .destructive(Text("Archive")) {
|
||||
Task { await archiveAllReportsForMe(chat.chatInfo.apiId) }
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
private func archiveAllReportsForMe(_ apiId: Int64) async {
|
||||
do {
|
||||
if case let .groupChatItemsDeleted(user, groupInfo, chatItemIDs, _, member) = try await apiArchiveReceivedReports(groupId: apiId) {
|
||||
await groupChatItemsDeleted(user, groupInfo, chatItemIDs, member)
|
||||
}
|
||||
} catch {
|
||||
logger.error("archiveAllReportsForMe error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
private func clearChatAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Clear conversation?"),
|
||||
|
||||
@@ -790,7 +790,12 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotificationData)?
|
||||
cItem = autoReceiveFile(file) ?? cItem
|
||||
}
|
||||
let ntf: NSENotificationData = (cInfo.ntfsEnabled(chatItem: cItem) && cItem.showNotification) ? .messageReceived(user, cInfo, cItem) : .noNtf
|
||||
return (chatItem.chatId, ntf)
|
||||
let chatIdOrMemberId = if case let .groupRcv(groupMember) = chatItem.chatItem.chatDir {
|
||||
groupMember.id
|
||||
} else {
|
||||
chatItem.chatInfo.id
|
||||
}
|
||||
return (chatIdOrMemberId, ntf)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ public enum ChatCommand {
|
||||
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool)
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode)
|
||||
case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64])
|
||||
case apiArchiveReceivedReports(groupId: Int64)
|
||||
case apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode)
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction)
|
||||
case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64])
|
||||
@@ -230,6 +232,8 @@ public enum ChatCommand {
|
||||
case let .apiUpdateChatItem(type, id, itemId, um, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(um.cmdString)"
|
||||
case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
|
||||
case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
case let .apiArchiveReceivedReports(groupId): return "/_archive reports #\(groupId)"
|
||||
case let .apiDeleteReceivedReports(groupId, itemIds, mode): return "/_delete reports #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
|
||||
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
|
||||
case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))"
|
||||
case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
@@ -404,6 +408,8 @@ public enum ChatCommand {
|
||||
case .apiDeleteChatItem: return "apiDeleteChatItem"
|
||||
case .apiConnectContactViaAddress: return "apiConnectContactViaAddress"
|
||||
case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem"
|
||||
case .apiArchiveReceivedReports: return "apiArchiveReceivedReports"
|
||||
case .apiDeleteReceivedReports: return "apiDeleteReceivedReports"
|
||||
case .apiChatItemReaction: return "apiChatItemReaction"
|
||||
case .apiGetReactionMembers: return "apiGetReactionMembers"
|
||||
case .apiPlanForwardChatItems: return "apiPlanForwardChatItems"
|
||||
|
||||
@@ -2435,13 +2435,6 @@ public struct ChatItemDeletion: Decodable, Hashable {
|
||||
public struct AChatItem: Decodable, Hashable {
|
||||
public var chatInfo: ChatInfo
|
||||
public var chatItem: ChatItem
|
||||
|
||||
public var chatId: String {
|
||||
if case let .groupRcv(groupMember) = chatItem.chatDir {
|
||||
return groupMember.id
|
||||
}
|
||||
return chatInfo.id
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIMentionMember: Decodable, Hashable {
|
||||
|
||||
@@ -203,6 +203,11 @@ func hideSecrets(_ cItem: ChatItem) -> String {
|
||||
}
|
||||
return res
|
||||
} else {
|
||||
return cItem.text
|
||||
let mc = cItem.content.msgContent
|
||||
if case let .report(text, reason) = mc {
|
||||
return NSLocalizedString("Report: \(text.isEmpty ? reason.text : text)", comment: "report in notification")
|
||||
} else {
|
||||
return cItem.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -851,8 +851,8 @@ object ChatModel {
|
||||
changeGroupReportsCounter(rhId, chatId, 1)
|
||||
}
|
||||
|
||||
fun decreaseGroupReportsCounter(rhId: Long?, chatId: ChatId) {
|
||||
changeGroupReportsCounter(rhId, chatId, -1)
|
||||
fun decreaseGroupReportsCounter(rhId: Long?, chatId: ChatId, by: Int = 1) {
|
||||
changeGroupReportsCounter(rhId, chatId, -by)
|
||||
}
|
||||
|
||||
private fun changeGroupReportsCounter(rhId: Long?, chatId: ChatId, by: Int = 0) {
|
||||
|
||||
+75
-48
@@ -1071,6 +1071,20 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiArchiveReceivedReports(rh: Long?, groupId: Long): CR.GroupChatItemsDeleted? {
|
||||
val r = sendCmd(rh, CC.ApiArchiveReceivedReports(groupId))
|
||||
if (r is CR.GroupChatItemsDeleted) return r
|
||||
Log.e(TAG, "apiArchiveReceivedReports bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiDeleteReceivedReports(rh: Long?, groupId: Long, itemIds: List<Long>, mode: CIDeleteMode): List<ChatItemDeletion>? {
|
||||
val r = sendCmd(rh, CC.ApiDeleteReceivedReports(groupId, itemIds, mode))
|
||||
if (r is CR.ChatItemsDeleted) return r.chatItemDeletions
|
||||
Log.e(TAG, "apiDeleteReceivedReports bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun testProtoServer(rh: Long?, server: String): ProtocolTestFailure? {
|
||||
val userId = currentUserId("testProtoServer")
|
||||
val r = sendCmd(rh, CC.APITestProtoServer(userId, server))
|
||||
@@ -2606,6 +2620,9 @@ object ChatController {
|
||||
} else {
|
||||
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
}
|
||||
if (cItem.isActiveReport) {
|
||||
decreaseGroupReportsCounter(rhId, cInfo.id)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (cItem.isReport) {
|
||||
@@ -2619,54 +2636,7 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
is CR.GroupChatItemsDeleted -> {
|
||||
if (!active(r.user)) {
|
||||
val users = chatController.listUsers(rhId)
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
return
|
||||
}
|
||||
val cInfo = ChatInfo.Group(r.groupInfo)
|
||||
withChats {
|
||||
r.chatItemIDs.forEach { itemId ->
|
||||
decreaseGroupReportsCounter(rhId, cInfo.id)
|
||||
val cItem = chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach
|
||||
if (chatModel.chatId.value != null) {
|
||||
// Stop voice playback only inside a chat, allow to play in a chat list
|
||||
AudioPlayer.stop(cItem)
|
||||
}
|
||||
val isLastChatItem = getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
|
||||
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
|
||||
ntfManager.cancelNotificationsForChat(cInfo.id)
|
||||
ntfManager.displayNotification(
|
||||
r.user,
|
||||
cInfo.id,
|
||||
cInfo.displayName,
|
||||
generalGetString(MR.strings.marked_deleted_description)
|
||||
)
|
||||
}
|
||||
val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember?.groupMemberId != r.member_.groupMemberId) {
|
||||
CIDeleted.Moderated(Clock.System.now(), r.member_)
|
||||
} else {
|
||||
CIDeleted.Deleted(Clock.System.now())
|
||||
}
|
||||
upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
r.chatItemIDs.forEach { itemId ->
|
||||
val cItem = chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach
|
||||
if (chatModel.chatId.value != null) {
|
||||
// Stop voice playback only inside a chat, allow to play in a chat list
|
||||
AudioPlayer.stop(cItem)
|
||||
}
|
||||
val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember?.groupMemberId != r.member_.groupMemberId) {
|
||||
CIDeleted.Moderated(Clock.System.now(), r.member_)
|
||||
} else {
|
||||
CIDeleted.Deleted(Clock.System.now())
|
||||
}
|
||||
upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
|
||||
}
|
||||
}
|
||||
groupChatItemsDeleted(rhId, r)
|
||||
}
|
||||
is CR.ReceivedGroupInvitation -> {
|
||||
if (active(r.user)) {
|
||||
@@ -3150,6 +3120,57 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun groupChatItemsDeleted(rhId: Long?, r: CR.GroupChatItemsDeleted) {
|
||||
if (!activeUser(rhId, r.user)) {
|
||||
val users = chatController.listUsers(rhId)
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
return
|
||||
}
|
||||
val cInfo = ChatInfo.Group(r.groupInfo)
|
||||
withChats {
|
||||
r.chatItemIDs.forEach { itemId ->
|
||||
decreaseGroupReportsCounter(rhId, cInfo.id)
|
||||
val cItem = chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach
|
||||
if (chatModel.chatId.value != null) {
|
||||
// Stop voice playback only inside a chat, allow to play in a chat list
|
||||
AudioPlayer.stop(cItem)
|
||||
}
|
||||
val isLastChatItem = getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
|
||||
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
|
||||
ntfManager.cancelNotificationsForChat(cInfo.id)
|
||||
ntfManager.displayNotification(
|
||||
r.user,
|
||||
cInfo.id,
|
||||
cInfo.displayName,
|
||||
generalGetString(MR.strings.marked_deleted_description)
|
||||
)
|
||||
}
|
||||
val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember?.groupMemberId != r.member_.groupMemberId) {
|
||||
CIDeleted.Moderated(Clock.System.now(), r.member_)
|
||||
} else {
|
||||
CIDeleted.Deleted(Clock.System.now())
|
||||
}
|
||||
upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
r.chatItemIDs.forEach { itemId ->
|
||||
val cItem = chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach
|
||||
if (chatModel.chatId.value != null) {
|
||||
// Stop voice playback only inside a chat, allow to play in a chat list
|
||||
AudioPlayer.stop(cItem)
|
||||
}
|
||||
val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember?.groupMemberId != r.member_.groupMemberId) {
|
||||
CIDeleted.Moderated(Clock.System.now(), r.member_)
|
||||
} else {
|
||||
CIDeleted.Deleted(Clock.System.now())
|
||||
}
|
||||
upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun chatItemUpdateNotify(rh: Long?, user: UserLike, aChatItem: AChatItem) {
|
||||
val cInfo = aChatItem.chatInfo
|
||||
val cItem = aChatItem.chatItem
|
||||
@@ -3375,6 +3396,8 @@ sealed class CC {
|
||||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC()
|
||||
class ApiArchiveReceivedReports(val groupId: Long): CC()
|
||||
class ApiDeleteReceivedReports(val groupId: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC()
|
||||
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List<Long>): CC()
|
||||
@@ -3553,6 +3576,8 @@ sealed class CC {
|
||||
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}"
|
||||
is ApiArchiveReceivedReports -> "/_archive reports #$groupId"
|
||||
is ApiDeleteReceivedReports -> "/_delete reports #$groupId ${itemIds.joinToString(",")} ${mode.deleteMode}"
|
||||
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
|
||||
is ApiGetReactionMembers -> "/_reaction members $userId #$groupId $itemId ${json.encodeToString(reaction)}"
|
||||
is ApiForwardChatItems -> {
|
||||
@@ -3719,6 +3744,8 @@ sealed class CC {
|
||||
is ApiUpdateChatItem -> "apiUpdateChatItem"
|
||||
is ApiDeleteChatItem -> "apiDeleteChatItem"
|
||||
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
|
||||
is ApiArchiveReceivedReports -> "apiArchiveReceivedReports"
|
||||
is ApiDeleteReceivedReports -> "apiDeleteReceivedReports"
|
||||
is ApiChatItemReaction -> "apiChatItemReaction"
|
||||
is ApiGetReactionMembers -> "apiGetReactionMembers"
|
||||
is ApiForwardChatItems -> "apiForwardChatItems"
|
||||
|
||||
+6
-1
@@ -134,7 +134,12 @@ abstract class NtfManager {
|
||||
}
|
||||
res
|
||||
} else {
|
||||
cItem.text
|
||||
val mc = cItem.content.msgContent
|
||||
if (mc is MsgContent.MCReport) {
|
||||
generalGetString(MR.strings.notification_group_report).format(cItem.text.ifEmpty { mc.reason.text })
|
||||
} else {
|
||||
cItem.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+62
-2
@@ -196,6 +196,7 @@ fun ChatView(
|
||||
)
|
||||
}
|
||||
},
|
||||
archiveItems = { archiveItems(chatRh, chatInfo, selectedChatItems) },
|
||||
moderateItems = {
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
val itemIds = selectedChatItems.value
|
||||
@@ -397,6 +398,7 @@ fun ChatView(
|
||||
}
|
||||
},
|
||||
deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) },
|
||||
archiveReports = { itemIds, forAll -> archiveReports(chatRh, chatInfo, itemIds, forAll) },
|
||||
receiveFile = { fileId ->
|
||||
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) }
|
||||
},
|
||||
@@ -673,6 +675,7 @@ fun ChatLayout(
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
archiveReports: (List<Long>, Boolean) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
@@ -745,7 +748,7 @@ fun ChatLayout(
|
||||
}) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
|
||||
useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, archiveReports,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
|
||||
@@ -792,6 +795,7 @@ fun ChatLayout(
|
||||
})
|
||||
}
|
||||
},
|
||||
archiveItems = { archiveItems(remoteHostId, chatInfo, selectedChatItems) },
|
||||
moderateItems = {},
|
||||
forwardItems = {}
|
||||
)
|
||||
@@ -1138,6 +1142,7 @@ fun BoxScope.ChatItemsList(
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
archiveReports: (List<Long>, Boolean) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
@@ -1291,7 +1296,7 @@ fun BoxScope.ChatItemsList(
|
||||
highlightedItems.value = setOf()
|
||||
}
|
||||
}
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2368,6 +2373,59 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long
|
||||
}
|
||||
}
|
||||
|
||||
private fun archiveReports(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long>, forAll: Boolean, onSuccess: () -> Unit = {}) {
|
||||
if (itemIds.isNotEmpty()) {
|
||||
withBGApi {
|
||||
val deleted = chatModel.controller.apiDeleteReceivedReports(
|
||||
chatRh,
|
||||
groupId = chatInfo.apiId,
|
||||
itemIds = itemIds,
|
||||
mode = if (forAll) CIDeleteMode.cidmBroadcast else CIDeleteMode.cidmInternalMark
|
||||
)
|
||||
if (deleted != null) {
|
||||
withChats {
|
||||
for (di in deleted) {
|
||||
val toChatItem = di.toChatItem?.chatItem
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
val deletedItem = di.deletedChatItem.chatItem
|
||||
if (deletedItem.isActiveReport) {
|
||||
decreaseGroupReportsCounter(chatRh, chatInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
for (di in deleted) {
|
||||
if (di.deletedChatItem.chatItem.isReport) {
|
||||
val toChatItem = di.toChatItem?.chatItem
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun archiveItems(rhId: Long?, chatInfo: ChatInfo, selectedChatItems: MutableState<Set<Long>?>) {
|
||||
val itemIds = selectedChatItems.value
|
||||
if (itemIds != null) {
|
||||
showArchiveReportsAlert(itemIds.sorted(), chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberActive, archiveReports = { ids, forAll ->
|
||||
archiveReports(rhId, chatInfo, ids, forAll) {
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun markUnreadChatAsRead(chatId: String) {
|
||||
val chat = chatModel.chats.value.firstOrNull { it.id == chatId }
|
||||
if (chat?.chatStats?.unreadChat != true) return
|
||||
@@ -2716,6 +2774,7 @@ fun PreviewChatLayout() {
|
||||
loadMessages = { _, _, _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
archiveReports = { _, _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
@@ -2791,6 +2850,7 @@ fun PreviewGroupChatLayout() {
|
||||
loadMessages = { _, _, _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = {},
|
||||
archiveReports = { _, _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
|
||||
+8
-2
@@ -53,11 +53,13 @@ fun SelectedItemsBottomToolbar(
|
||||
contentTag: MsgContentTag?,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
|
||||
archiveItems: () -> Unit,
|
||||
moderateItems: () -> Unit,
|
||||
forwardItems: () -> Unit,
|
||||
) {
|
||||
val deleteEnabled = remember { mutableStateOf(false) }
|
||||
val deleteForEveryoneEnabled = remember { mutableStateOf(false) }
|
||||
val canArchiveReports = remember { mutableStateOf(false) }
|
||||
val canModerate = remember { mutableStateOf(false) }
|
||||
val moderateEnabled = remember { mutableStateOf(false) }
|
||||
val forwardEnabled = remember { mutableStateOf(false) }
|
||||
@@ -80,7 +82,7 @@ fun SelectedItemsBottomToolbar(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !deleteCountProhibited.value) {
|
||||
IconButton({ if (canArchiveReports.value) archiveItems() else deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !deleteCountProhibited.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_delete),
|
||||
null,
|
||||
@@ -111,7 +113,7 @@ fun SelectedItemsBottomToolbar(
|
||||
}
|
||||
val chatItems = remember { derivedStateOf { chatModel.chatItemsForContent(contentTag).value } }
|
||||
LaunchedEffect(chatInfo, chatItems.value, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, chatItems.value, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||
recheckItems(chatInfo, chatItems.value, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canArchiveReports, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +122,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
deleteEnabled: MutableState<Boolean>,
|
||||
deleteForEveryoneEnabled: MutableState<Boolean>,
|
||||
canArchiveReports: MutableState<Boolean>,
|
||||
canModerate: MutableState<Boolean>,
|
||||
moderateEnabled: MutableState<Boolean>,
|
||||
forwardEnabled: MutableState<Boolean>,
|
||||
@@ -133,6 +136,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
val selected = selectedChatItems.value ?: return
|
||||
var rDeleteEnabled = true
|
||||
var rDeleteForEveryoneEnabled = true
|
||||
var rCanArchiveReports = true
|
||||
var rModerateEnabled = true
|
||||
var rOnlyOwnGroupItems = true
|
||||
var rForwardEnabled = true
|
||||
@@ -141,6 +145,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
if (selected.contains(ci.id)) {
|
||||
rDeleteEnabled = rDeleteEnabled && ci.canBeDeletedForSelf
|
||||
rDeleteForEveryoneEnabled = rDeleteForEveryoneEnabled && ci.meta.deletable && !ci.localNote && !ci.isReport
|
||||
rCanArchiveReports = rCanArchiveReports && ci.isActiveReport && ci.chatDir !is CIDirection.GroupSnd && chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator
|
||||
rOnlyOwnGroupItems = rOnlyOwnGroupItems && ci.chatDir is CIDirection.GroupSnd && !ci.isReport
|
||||
rModerateEnabled = rModerateEnabled && ci.content.msgContent != null && ci.memberToModerate(chatInfo) != null && !ci.isReport
|
||||
rForwardEnabled = rForwardEnabled && ci.content.msgContent != null && ci.meta.itemDeleted == null && !ci.isLiveDummy && !ci.isReport
|
||||
@@ -150,6 +155,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
rModerateEnabled = rModerateEnabled && !rOnlyOwnGroupItems
|
||||
deleteEnabled.value = rDeleteEnabled
|
||||
deleteForEveryoneEnabled.value = rDeleteForEveryoneEnabled
|
||||
canArchiveReports.value = rCanArchiveReports
|
||||
moderateEnabled.value = rModerateEnabled
|
||||
forwardEnabled.value = rForwardEnabled
|
||||
selectedChatItems.value = rSelectedChatItems
|
||||
|
||||
+45
-12
@@ -77,6 +77,7 @@ fun ChatItemView(
|
||||
selectChatItem: () -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
archiveReports: (List<Long>, Boolean) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
@@ -301,7 +302,7 @@ fun ChatItemView(
|
||||
cItem.isReport && cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group -> {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
ArchiveReportItemAction(cItem, showMenu, deleteMessage)
|
||||
ArchiveReportItemAction(cItem.id, cInfo.groupInfo.membership.memberActive, showMenu, archiveReports)
|
||||
}
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report))
|
||||
Divider()
|
||||
@@ -914,23 +915,53 @@ private fun ReportItemAction(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveReportItemAction(cItem: ChatItem, showMenu: MutableState<Boolean>, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
private fun ArchiveReportItemAction(id: Long, allowForAll: Boolean, showMenu: MutableState<Boolean>, archiveReports: (List<Long>, Boolean) -> Unit) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.archive_report),
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.report_archive_alert_title),
|
||||
text = generalGetString(MR.strings.report_archive_alert_desc),
|
||||
onConfirm = {
|
||||
deleteMessage(cItem.id, CIDeleteMode.cidmInternalMark)
|
||||
},
|
||||
destructive = true,
|
||||
confirmText = generalGetString(MR.strings.archive_verb),
|
||||
)
|
||||
showArchiveReportsAlert(listOf(id), allowForAll, archiveReports)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun showArchiveReportsAlert(ids: List<Long>, allowForAll: Boolean, archiveReports: (List<Long>, Boolean) -> Unit) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = if (ids.size == 1) {
|
||||
generalGetString(MR.strings.report_archive_alert_title)
|
||||
} else {
|
||||
generalGetString(MR.strings.report_archive_alert_title_nth).format(ids.size)
|
||||
},
|
||||
color = Color.Red
|
||||
text = null,
|
||||
buttons = {
|
||||
// Archive for me
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
archiveReports(ids, false)
|
||||
}) {
|
||||
Text(
|
||||
generalGetString(MR.strings.report_archive_for_me),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
if (allowForAll) {
|
||||
// Archive for all moderators
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
archiveReports(ids, true)
|
||||
}) {
|
||||
Text(
|
||||
stringResource(MR.strings.report_archive_for_all_moderators),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1310,6 +1341,7 @@ fun PreviewChatItemView(
|
||||
selectChatItem = {},
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
archiveReports = { _, _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
@@ -1356,6 +1388,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
selectChatItem = {},
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
archiveReports = { _, _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = { _, _ -> },
|
||||
|
||||
+37
@@ -19,6 +19,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
|
||||
import chat.simplex.common.platform.*
|
||||
@@ -299,6 +300,11 @@ fun GroupMenuItems(
|
||||
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
||||
ToggleNotificationsChatAction(chat, chatModel, groupInfo.chatSettings.enableNtfs.nextMode(true), showMenu)
|
||||
TagListAction(chat, showMenu)
|
||||
if (chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
ArchiveAllReportsItemAction(showMenu) {
|
||||
archiveAllReportsForMe(chat.remoteHostId, chat.chatInfo.apiId)
|
||||
}
|
||||
}
|
||||
ClearChatAction(chat, showMenu)
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
||||
@@ -563,6 +569,18 @@ private fun InvalidDataView() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveAllReportsItemAction(showMenu: MutableState<Boolean>, archiveReports: () -> Unit) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.archive_reports),
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
onClick = {
|
||||
showArchiveAllReportsForMeAlert(archiveReports)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun markChatRead(c: Chat) {
|
||||
var chat = c
|
||||
withApi {
|
||||
@@ -886,6 +904,25 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
|
||||
}
|
||||
}
|
||||
|
||||
private fun showArchiveAllReportsForMeAlert(archiveReports: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.report_archive_alert_title_all),
|
||||
text = generalGetString(MR.strings.report_archive_alert_desc_all),
|
||||
onConfirm = archiveReports,
|
||||
destructive = true,
|
||||
confirmText = generalGetString(MR.strings.archive_verb),
|
||||
)
|
||||
}
|
||||
|
||||
private fun archiveAllReportsForMe(chatRh: Long?, apiId: Long) {
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiArchiveReceivedReports(chatRh, apiId)
|
||||
if (r != null) {
|
||||
controller.groupChatItemsDeleted(chatRh, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun ChatListNavLinkLayout(
|
||||
chatLinkPreview: @Composable () -> Unit,
|
||||
|
||||
@@ -312,7 +312,12 @@
|
||||
<string name="message_deleted_or_not_received_error_desc">This message was deleted or not received yet.</string>
|
||||
<string name="report_reason_alert_title">Report reason?</string>
|
||||
<string name="report_archive_alert_title">Archive report?</string>
|
||||
<string name="report_archive_alert_title_nth">Archive %d reports?</string>
|
||||
<string name="report_archive_alert_title_all">Archive all reports?</string>
|
||||
<string name="report_archive_alert_desc">The report will be archived for you.</string>
|
||||
<string name="report_archive_alert_desc_all">All reports will be archived for you.</string>
|
||||
<string name="report_archive_for_me">For me</string>
|
||||
<string name="report_archive_for_all_moderators">For all moderators</string>
|
||||
|
||||
<!-- CIStatus errors -->
|
||||
<string name="ci_status_other_error">Error: %1$s</string>
|
||||
@@ -341,6 +346,7 @@
|
||||
<string name="search_verb">Search</string>
|
||||
<string name="archive_verb">Archive</string>
|
||||
<string name="archive_report">Archive report</string>
|
||||
<string name="archive_reports">Archive reports</string>
|
||||
<string name="delete_report">Delete report</string>
|
||||
<string name="sent_message">Sent message</string>
|
||||
<string name="received_message">Received message</string>
|
||||
@@ -447,6 +453,7 @@
|
||||
<string name="chat_list_businesses">Businesses</string>
|
||||
<string name="chat_list_notes">Notes</string>
|
||||
<string name="chat_list_group_reports">Reports</string>
|
||||
<string name="notification_group_report">Report: %s</string>
|
||||
<string name="chat_list_all">All</string>
|
||||
<string name="chat_list_add_list">Add list</string>
|
||||
<string name="group_reports_active_one">1 report</string>
|
||||
|
||||
@@ -3740,8 +3740,8 @@ chatCommandP =
|
||||
"/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <*> (" json" *> jsonP <|> " text " *> updatedMessagesTextP)),
|
||||
"/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <*> _strP),
|
||||
"/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP),
|
||||
"/_archive reports " *> (APIArchiveReceivedReports <$> A.decimal),
|
||||
"/_delete reports " *> (APIDeleteReceivedReports <$> A.decimal <*> _strP <*> _strP),
|
||||
"/_archive reports #" *> (APIArchiveReceivedReports <$> A.decimal),
|
||||
"/_delete reports #" *> (APIDeleteReceivedReports <$> A.decimal <*> _strP <*> _strP),
|
||||
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP),
|
||||
"/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP),
|
||||
"/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP),
|
||||
|
||||
@@ -6031,12 +6031,12 @@ testGroupMemberReports =
|
||||
bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content"), (0, "report spam")])
|
||||
cath #$> ("/_get chat #1 content=report count=100", chat, [])
|
||||
dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content"), (1, "report spam")])
|
||||
alice ##> "/_archive reports 1"
|
||||
alice ##> "/_archive reports #1"
|
||||
alice <## "#jokes: 2 messages deleted by user"
|
||||
(bob </)
|
||||
alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]"), (0, "report spam [marked deleted by you]")])
|
||||
bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content"), (0, "report spam")])
|
||||
bob ##> "/_archive reports 1"
|
||||
bob ##> "/_archive reports #1"
|
||||
bob <## "#jokes: 2 messages deleted by user"
|
||||
bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]"), (0, "report spam [marked deleted by you]")])
|
||||
-- delete reports for all admins
|
||||
@@ -6063,7 +6063,7 @@ testGroupMemberReports =
|
||||
]
|
||||
alice ##> "/last_item_id"
|
||||
i :: ChatItemId <- read <$> getTermLine alice
|
||||
alice ##> ("/_delete reports 1 " <> show i <> " broadcast")
|
||||
alice ##> ("/_delete reports #1 " <> show i <> " broadcast")
|
||||
alice <## "message marked deleted by you"
|
||||
bob <# "#jokes dan> [marked deleted by alice] report content"
|
||||
alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]")])
|
||||
|
||||
Reference in New Issue
Block a user