Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Monal/Classes/ActiveChatsViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,8 @@ -(void) viewDidLoad
[nc addObserver:self selector:@selector(handleRefreshDisplayNotification:) name:kMonalMessageFiletransferUpdateNotice object:nil];
[nc addObserver:self selector:@selector(refreshContact:) name:kMonalContactRefresh object:nil];
[nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalNewMessageNotice object:nil];
[nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalUpdatedMessageNotice object:nil];
[nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalDeletedMessageNotice object:nil];
[nc addObserver:self selector:@selector(refreshContact:) name:kMonalUpdatedMessageNotice object:nil];
[nc addObserver:self selector:@selector(refreshContact:) name:kMonalDeletedMessageNotice object:nil];
[nc addObserver:self selector:@selector(messageSent:) name:kMLMessageSentToContact object:nil];
[nc addObserver:self selector:@selector(handleDeviceRotation) name:UIDeviceOrientationDidChangeNotification object:nil];
[nc addObserver:self selector:@selector(showWarningsIfNeeded) name:kMonalFinishedCatchup object:nil];
Expand Down
197 changes: 195 additions & 2 deletions Monal/Classes/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,83 @@ struct ChatView: View {
@State private var alertPrompt: AlertPrompt?
@State private var confirmationPrompt: ConfirmationPrompt?
@StateObject private var overlay = LoadingOverlayState()
@State private var moderationReason = "Spam"
@State private var messageToModerate: MLMessage?
@State var messages: [ChatViewMessage] = []
private var account: xmpp

init(contact: ObservableKVOWrapper<MLContact>) {
_contact = StateObject(wrappedValue: contact)
account = contact.obj.account!
}


enum MessageAction: MessageMenuAction {
case copy, edit, retract, moderate, delete, resend

func title() -> String {
switch self {
case .copy:
"Copy"
case .edit:
"Edit"
case .retract:
"Retract"
case .moderate:
"Moderate"
case .delete:
"Delete Locally"
case .resend:
"Resend"
}
}

func icon() -> Image {
switch self {
case .copy:
Image(systemName: "doc.on.doc")
case .edit:
if #available(iOS 18.0, macCatalyst 18.0, *) {
Image(systemName: "bubble.and.pencil")
} else {
Image(systemName: "square.and.pencil")
}
case .retract, .moderate:
Image(systemName: "arrow.uturn.backward.circle")
case .delete:
Image(systemName: "trash")
case .resend:
Image(systemName: "paperplane")
}
}

static func menuItems(for message: ExyteChat.Message) -> [MessageAction] {
let mlMessage = (message as! ChatViewMessage).innerMessage.obj
let contact = mlMessage.contact
let account = contact.account!
if mlMessage.retracted {
return [.delete]
}
if case .error = message.status {
return [.resend, .delete]
}
var availableActions: [MessageAction] = [.copy]
if !mlMessage.inbound && DataLayer.sharedInstance().checkLMCEligible(mlMessage.messageDBId, encrypted: mlMessage.encrypted || contact.isEncrypted, historyBaseID: nil) {
availableActions.append(.edit)
}

if !mlMessage.inbound && (!mlMessage.isMuc || (mlMessage.isMuc && !mlMessage.stanzaId.isEmpty)) {
availableActions.append(.retract)
}
else if mlMessage.isMuc && !mlMessage.stanzaId.isEmpty && DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact) == kMucRoleModerator && account.mucProcessor.getRoomFeatures(forMuc: contact.contactJid).contains("urn:xmpp:message-moderate:1") {
availableActions.append(.moderate)
} else {
availableActions.append(.delete)
}

return availableActions
}
}

private func showCannotEncryptAlert(_ show: Bool) {
if show {
DDLogVerbose("Showing cannot encrypt alert...")
Expand Down Expand Up @@ -203,6 +272,93 @@ struct ChatView: View {
messages.append(ChatViewMessage(newMLMessage))
} messageBuilder: { message, viewModel, positionInUserGroup, positionInMessagesSection, positionInCommentsGroup, showContextMenuClosure, messageActionClosure, showAttachmentClosure in
MessageView(message: (message as! ChatViewMessage), viewModel: viewModel, positionInUserGroup: positionInUserGroup, positionInMessagesSection: positionInMessagesSection)
} messageMenuAction: { (action: MessageAction, defaultActionClosure, message) in
let mlMessage = (message as! ChatViewMessage).innerMessage.obj
let messageDBId = mlMessage.messageDBId
switch action {
case .copy:
defaultActionClosure(message, .copy)
case .edit:
defaultActionClosure(message, .edit { editedText in
Task { @MainActor in
self.account.sendMessage(editedText,
to: self.contact.obj,
isEncrypted: self.contact.isEncrypted || mlMessage.encrypted,
isUpload: false,
andMessageId: UUID().uuidString,
withLMCId: mlMessage.messageId)

// Don't block the main thread while writing to the DB
await Task.detached(priority: .userInitiated) {
DataLayer.sharedInstance().updateMessageHistory(messageDBId, withText: editedText)
}.value

MLNotificationQueue.current().post(
name: Notification.Name(kMonalUpdatedMessageNotice),
object: self.account,
userInfo: ["message": mlMessage,
"contact": self.contact.obj,
"LMCReplaced": true,
"correctedText": editedText,
]
)
}
})
case .retract:
self.account.retractMessage(mlMessage)
case .moderate:
messageToModerate = mlMessage
case .delete:
Task { @MainActor in
await Task.detached(priority: .userInitiated) {
DataLayer.sharedInstance().deleteMessageHistoryLocally(mlMessage.messageDBId)
}.value

self.messages.removeAll(where: {$0.id == message.id})
// Update active chats if necessary
MLNotificationQueue.current().post(
name: Notification.Name(kMonalContactRefresh),
object: self.account,
userInfo: ["contact": self.contact.obj]
)
}
case .resend:
confirmationPrompt = ConfirmationPrompt(
title: Text("Retry sending message?"),
message: Text("This message failed to send (\(mlMessage.errorType)): \(mlMessage.errorReason)"),
buttons: [
.default(
Text("Retry"),
action: {
Task { @MainActor in
await Task.detached(priority: .userInitiated) {
DataLayer.sharedInstance().clearError(ofMessageId: mlMessage.messageId)
}.value

mlMessage.errorType = ""
mlMessage.errorReason = ""
let isUpload = mlMessage.messageType == kMessageTypeFiletransfer
let isEncrypted = mlMessage.encrypted || self.contact.isEncrypted
self.account.sendMessage(mlMessage.messageText,
to: self.contact.obj,
isEncrypted: isEncrypted,
isUpload: isUpload,
andMessageId: mlMessage.messageId)
MLNotificationQueue.current().post(
name: Notification.Name(kMLMessageSentToContact),
object: self.account,
userInfo: ["contact": self.contact.obj]
)
}
}
),
.cancel(
Text("Cancel"),
action: { }
)
]
)
}
}
.showNetworkConnectionProblem(false)
// .enableLoadMore(pageSize: 3) { message in
Expand All @@ -228,6 +384,20 @@ struct ChatView: View {
}
}))
}
.alert("Moderating message", isPresented: $messageToModerate.optionalMappedToBool(), actions: {
TextField("Reason", text: $moderationReason)
Button("Moderate", role: .destructive) {
MLAssert(messageToModerate != nil, "messageToModerate must not be nil during moderation!")
self.account.moderateMessage(messageToModerate!, withReason: moderationReason)
// Reset the State variables to their default values, as the alert is dismissed
messageToModerate = nil
moderationReason = "Spam"
}
Button("Cancel", role: .cancel) {
messageToModerate = nil
moderationReason = "Spam"
}
}, message: { Text("Enter the moderation reason") })
.toolbar {
ToolbarItem(placement: .principal) {
//make sure to take all space available, otherwise we'll get aligned to the center
Expand Down Expand Up @@ -425,7 +595,30 @@ class ChatViewMessage: ExyteChat.Message {
private var subscriptions: Set<AnyCancellable> = Set()
override var text: String {
get {
return innerMessage.retracted ? "This message got retracted" : innerMessage.messageText
return innerMessage.retracted ? NSLocalizedString("This message got retracted", comment: "") : innerMessage.messageText
}
set {}
}
override var status: Status? {
get {
// Incoming messages shouldn't have a status
if innerMessage.inbound {
return nil
}
let errorType = innerMessage.errorType as String?
let isError = errorType != nil && !errorType!.isEmpty
switch(innerMessage) {
case let message where isError && !message.hasBeenReceived:
return .error(DraftMessage(id: id, text: text, medias: [], recording: recording, replyMessage: replyMessage, createdAt: createdAt))
case let message where message.hasBeenDisplayed:
return .read
case let message where message.hasBeenReceived:
return .received
case let message where message.hasBeenSent:
return .sent
default:
return .sending
}
}
set {}
}
Expand Down
8 changes: 4 additions & 4 deletions Monal/Classes/MLNotificationQueue.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ NS_ASSUME_NONNULL_BEGIN
-(NSUInteger) flush;
-(NSUInteger) clear;

+(id) currentQueue;
-(void) postNotificationName:(NSNotificationName) notificationName object:(id _Nullable) notificationObject userInfo:(id _Nullable) notificationUserInfo;
-(void) postNotificationName:(NSNotificationName) notificationName object:(id _Nullable) notificationObject;
-(void) postNotification:(NSNotification* _Nonnull) notification;
+(instancetype) currentQueue;
-(void) postNotificationName:(NSNotificationName) notificationName object:(id _Nullable) notificationObject userInfo:(id _Nullable) notificationUserInfo NS_SWIFT_NAME(post(name:object:userInfo:));
-(void) postNotificationName:(NSNotificationName) notificationName object:(id _Nullable) notificationObject NS_SWIFT_NAME(post(name:object:));
-(void) postNotification:(NSNotification* _Nonnull) notification NS_SWIFT_NAME(post(name:));

@property (readonly, strong) NSString* name;
-(NSString*) description;
Expand Down
4 changes: 2 additions & 2 deletions Monal/Classes/MLNotificationQueue.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ +(void) queueNotificationsInBlock:(monal_void_block_t) block onQueue:(NSString*)
queue = nil;
}

+(id) currentQueue
+(instancetype) currentQueue
{
NSMutableArray* stack = [self getThreadLocalNotificationQueueStack];
if(![stack count])
return [NSNotificationCenter defaultCenter];
return (MLNotificationQueue*) [NSNotificationCenter defaultCenter];
return [stack lastObject];
}

Expand Down
9 changes: 0 additions & 9 deletions Monal/Classes/chatViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -2472,19 +2472,10 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe
UIContextualAction* retractAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:NSLocalizedString(@"Retract", @"Chat msg action") handler:^(UIContextualAction* action, UIView* sourceView, void (^completionHandler)(BOOL actionPerformed)) {
//only delete directly if we sent that message, try to moderate otherwise
if(!message.inbound)
{
[self.xmppAccount retractMessage:message];
[[DataLayer sharedInstance] retractMessageHistory:message.messageDBId];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:self.xmppAccount userInfo:@{
@"message": message,
@"contact": self.contact
}];
}
else
{
//hardcode reason for now (change this when rewriting chatui using swiftui)
[self.xmppAccount moderateMessage:message withReason:@"This message contains inappropriate content for this forum."];
}

return completionHandler(YES);
}];
Expand Down
9 changes: 8 additions & 1 deletion Monal/Classes/xmpp.m
Original file line number Diff line number Diff line change
Expand Up @@ -3460,7 +3460,14 @@ -(void) retractMessage:(MLMessage*) msg
//for MAM
[messageNode setStoreHint];

[self send:messageNode];
[self dispatchAsyncOnReceiveQueue: ^{
[self send:messageNode];
[[DataLayer sharedInstance] retractMessageHistory:msg.messageDBId];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:self userInfo:@{
@"message": msg,
@"contact": msg.contact
}];
}];
}

-(void) moderateMessage:(MLMessage*) msg withReason:(NSString*) reason
Expand Down
12 changes: 6 additions & 6 deletions Monal/Monal.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading