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:
Stanislav Dmitrenko
2025-02-10 22:07:14 +07:00
committed by GitHub
parent c5bb2c4ca2
commit e7361cf025
19 changed files with 465 additions and 147 deletions
+1 -1
View File
@@ -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) {
+53 -33
View File
@@ -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()
+70 -12
View File
@@ -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
}
+6
View File
@@ -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"
-7
View File
@@ -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 {
+6 -1
View File
@@ -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
}
}
}
@@ -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) {
@@ -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"
@@ -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
}
}
}
}
@@ -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 = { _, _ -> },
@@ -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
@@ -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 = { _, _ -> },
@@ -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>
+2 -2
View File
@@ -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),
+3 -3
View File
@@ -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]")])