Merge pull request #484 from RyanRory/unsend-message

Unsend Requests
This commit is contained in:
Niels Andriesse 2021-08-10 11:16:00 +10:00 committed by GitHub
commit 8d8daf3a6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 912 additions and 33 deletions

View File

@ -136,6 +136,8 @@
768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; };
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; };
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; };
7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; };
@ -1098,6 +1100,8 @@
76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
76EB03C218170B33006006FC /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -2089,6 +2093,7 @@
B849789525D4A2F500D0D0B3 /* LinkPreviewView.swift */,
B8D84EA225DF745A005A043E /* LinkPreviewState.swift */,
B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */,
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */,
);
path = "Content Views";
sourceTree = "<group>";
@ -2377,6 +2382,7 @@
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */,
C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */,
C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */,
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */,
);
path = "Control Messages";
sourceTree = "<group>";
@ -4622,6 +4628,7 @@
C3A3A156256E1B91004D228D /* ProtoUtils.m in Sources */,
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */,
C352A32F2557549C00338F3E /* NotifyPNServerJob.swift in Sources */,
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */,
C300A5F22554B09800555489 /* MessageSender.swift in Sources */,
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */,
C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */,
@ -4903,6 +4910,7 @@
B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */,
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,

View File

@ -7,37 +7,37 @@ extension ContextMenuVC {
let work: () -> Void
static func reply(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = "Reply"
let title = NSLocalizedString("context_menu_reply", comment: "")
return Action(icon: UIImage(named: "ic_reply")!, title: title) { delegate?.reply(viewItem) }
}
static func copy(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = "Copy"
let title = NSLocalizedString("copy", comment: "")
return Action(icon: UIImage(named: "ic_copy")!, title: title) { delegate?.copy(viewItem) }
}
static func copySessionID(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = "Copy Session ID"
let title = NSLocalizedString("vc_conversation_settings_copy_session_id_button_title", comment: "")
return Action(icon: UIImage(named: "ic_copy")!, title: title) { delegate?.copySessionID(viewItem) }
}
static func delete(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = "Delete"
let title = NSLocalizedString("TXT_DELETE_TITLE", comment: "")
return Action(icon: UIImage(named: "ic_trash")!, title: title) { delegate?.delete(viewItem) }
}
static func save(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = "Save"
let title = NSLocalizedString("context_menu_save", comment: "")
return Action(icon: UIImage(named: "ic_download")!, title: title) { delegate?.save(viewItem) }
}
static func ban(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = "Ban User"
let title = NSLocalizedString("context_menu_ban_user", comment: "")
return Action(icon: UIImage(named: "ic_block")!, title: title) { delegate?.ban(viewItem) }
}
static func banAndDeleteAllMessages(_ viewItem: ConversationViewItem, _ delegate: ContextMenuActionDelegate?) -> Action {
let title = "Ban and Delete All"
let title = NSLocalizedString("context_menu_ban_and_delete_all", comment: "")
return Action(icon: UIImage(named: "ic_block")!, title: title) { delegate?.banAndDeleteAllMessages(viewItem) }
}
}
@ -82,7 +82,7 @@ extension ContextMenuVC {
}
// MARK: Delegate
protocol ContextMenuActionDelegate : class {
protocol ContextMenuActionDelegate : AnyObject {
func reply(_ viewItem: ConversationViewItem)
func copy(_ viewItem: ConversationViewItem)

View File

@ -555,7 +555,83 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
}
func delete(_ viewItem: ConversationViewItem) {
viewItem.deleteAction()
if (!self.isUnsendRequesEnabled) {
viewItem.deleteAction()
return
}
func showInputAccessoryView() {
UIView.animate(withDuration: 0.25, animations: {
self.inputAccessoryView?.isHidden = false
self.inputAccessoryView?.alpha = 1
})
}
if viewItem.interaction.interactionType() == .outgoingMessage,
let message = viewItem.interaction as? TSMessage, message.serverHash != nil {
let alertVC = UIAlertController.init(title: nil, message: nil, preferredStyle: .actionSheet)
let deleteLocallyAction = UIAlertAction.init(title: NSLocalizedString("delete_message_for_me", comment: ""), style: .destructive) { _ in
self.deleteLocally(viewItem)
showInputAccessoryView()
}
alertVC.addAction(deleteLocallyAction)
var title = NSLocalizedString("delete_message_for_everyone", comment: "")
if !viewItem.isGroupThread {
title = String(format: NSLocalizedString("delete_message_for_me_and_recipient", comment: ""), viewItem.interaction.thread.name())
}
let deleteRemotelyAction = UIAlertAction.init(title: title, style: .destructive) { _ in
self.deleteForEveryone(viewItem)
showInputAccessoryView()
}
alertVC.addAction(deleteRemotelyAction)
let cancelAction = UIAlertAction.init(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel) {_ in
showInputAccessoryView()
}
alertVC.addAction(cancelAction)
self.inputAccessoryView?.isHidden = true
self.inputAccessoryView?.alpha = 0
self.presentAlert(alertVC)
} else {
deleteLocally(viewItem)
}
}
private func buildUsendRequest(_ viewItem: ConversationViewItem) -> UnsendRequest? {
if let message = viewItem.interaction as? TSMessage,
message.isOpenGroupMessage || message.serverHash == nil { return nil }
let unsendRequest = UnsendRequest()
switch viewItem.interaction.interactionType() {
case .incomingMessage:
if let incomingMessage = viewItem.interaction as? TSIncomingMessage {
unsendRequest.author = incomingMessage.authorId
}
case .outgoingMessage: unsendRequest.author = getUserHexEncodedPublicKey()
default: return nil// Should never occur
}
unsendRequest.timestamp = viewItem.interaction.timestamp
return unsendRequest
}
func deleteLocally(_ viewItem: ConversationViewItem) {
viewItem.deleteLocallyAction()
if let unsendRequest = buildUsendRequest(viewItem) {
SNMessagingKitConfiguration.shared.storage.write { transaction in
MessageSender.send(unsendRequest, to: .contact(publicKey: getUserHexEncodedPublicKey()), using: transaction).retainUntilComplete()
}
}
}
func deleteForEveryone(_ viewItem: ConversationViewItem) {
viewItem.deleteLocallyAction()
viewItem.deleteRemotelyAction()
if let unsendRequest = buildUsendRequest(viewItem) {
SNMessagingKitConfiguration.shared.storage.write { transaction in
MessageSender.send(unsendRequest, in: self.thread, using: transaction as! YapDatabaseReadWriteTransaction)
}
}
}
func save(_ viewItem: ConversationViewItem) {

View File

@ -4,7 +4,8 @@
// Photo rounding (the small corners don't have the correct rounding)
// Remaining search glitchiness
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
let isUnsendRequesEnabled = false // Switch this to true if unsend request is done on all platforms
let thread: TSThread
let focusedMessageID: String? // This isn't actually used ATM
var unreadViewItems: [ConversationViewItem] = []
@ -484,7 +485,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
func updateUnreadCountView() {
let visibleViewItems = (messagesTableView.indexPathsForVisibleRows ?? []).map { viewItems[$0.row] }
let visibleViewItems = (messagesTableView.indexPathsForVisibleRows ?? []).map { viewItems[ifValid: $0.row] }
for visibleItem in visibleViewItems {
guard let index = unreadViewItems.firstIndex(where: { $0 === visibleItem }) else { continue }
unreadViewItems.remove(at: index)

View File

@ -15,6 +15,7 @@ typedef NS_ENUM(NSInteger, OWSMessageCellType) {
OWSMessageCellType_GenericAttachment,
OWSMessageCellType_MediaMessage,
OWSMessageCellType_OversizeTextDownloading,
OWSMessageCellType_DeletedMessage
};
NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@ -132,7 +133,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
- (void)copyTextAction;
- (void)shareMediaAction;
- (void)saveMediaAction;
- (void)deleteAction;
- (void)deleteLocallyAction;
- (void)deleteRemotelyAction;
- (void)deleteAction; // Remove this after the unsend request is enabled
- (BOOL)canCopyMedia;
- (BOOL)canSaveMedia;

View File

@ -470,6 +470,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
self.hasViewState = YES;
TSMessage *message = (TSMessage *)self.interaction;
if (message.isDeleted) {
self.messageCellType = OWSMessageCellType_DeletedMessage;
return;
}
// Check for quoted replies _before_ media album handling,
// since that logic may exit early.
@ -967,6 +972,66 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return [self saveMediaAlbumItems:mediaAlbumItems];
}
- (void)deleteLocallyAction
{
TSMessage *message = (TSMessage *)self.interaction;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[MessageInvalidator invalidate:message with:transaction];
[self.interaction removeWithTransaction:transaction];
if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
[LKStorage.shared cancelPendingMessageSendJobIfNeededForMessage:self.interaction.timestamp using:transaction];
}
}];
}
- (void)deleteRemotelyAction
{
TSMessage *message = (TSMessage *)self.interaction;
if (self.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
// Only allow deletion on incoming and outgoing messages
OWSInteractionType interationType = self.interaction.interactionType;
if (interationType != OWSInteractionType_IncomingMessage && interationType != OWSInteractionType_OutgoingMessage) return;
if (groupThread.isOpenGroup) {
// Make sure it's an open group message
if (!message.isOpenGroupMessage) return;
// Get the open group
SNOpenGroupV2 *openGroupV2 = [LKStorage.shared getV2OpenGroupForThreadID:groupThread.uniqueId];
if (openGroupV2 == nil) return;
// If it's an incoming message the user must have moderator status
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
if (![SNOpenGroupAPIV2 isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; }
}
// Delete the message
[[SNOpenGroupAPIV2 deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroupV2.room onServer:openGroupV2.server].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
} else {
NSString *groupPublicKey = [LKGroupUtilities getDecodedGroupID:groupThread.groupModel.groupId];
[[SNSnodeAPI deleteMessageForPublickKey:groupPublicKey serverHashes:@[message.serverHash]].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
}
} else {
TSContactThread *contactThread = (TSContactThread *)self.interaction.thread;
[[SNSnodeAPI deleteMessageForPublickKey:contactThread.contactSessionID serverHashes:@[message.serverHash]].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
}
}
// Remove this after the unsend request is enabled
- (void)deleteAction
{
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {

View File

@ -830,6 +830,17 @@ NS_ASSUME_NONNULL_BEGIN
[updatedNeighborItemSet addObject:itemId];
}
}
// Add the following item of a deleted message to update
// to show the date header of the deleted message if needed
for (NSString *deletedItemId in deletedItemIdSet) {
NSUInteger oldIndex = [oldItemIdList indexOfObject:deletedItemId];
if (oldIndex != NSNotFound && oldIndex + 1 < oldItemIdList.count) {
NSString *nextItemId = oldItemIdList[oldIndex + 1];
[updatedItemSet addObject:nextItemId];
[updatedNeighborItemSet addObject:nextItemId];
}
}
}
// 3. Updates.

View File

@ -0,0 +1,51 @@
final class DeletedMessageView : UIView {
private let viewItem: ConversationViewItem
private let textColor: UIColor
// MARK: Settings
private static let iconSize: CGFloat = 18
private static let iconImageViewSize: CGFloat = 30
// MARK: Lifecycle
init(viewItem: ConversationViewItem, textColor: UIColor) {
self.viewItem = viewItem
self.textColor = textColor
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(viewItem:textColor:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(viewItem:textColor:) instead.")
}
private func setUpViewHierarchy() {
// Image view
let iconSize = DeletedMessageView.iconSize
let icon = UIImage(named: "ic_trash")?.withTint(textColor)?.resizedImage(to: CGSize(width: iconSize, height: iconSize))
let imageView = UIImageView(image: icon)
imageView.contentMode = .center
let iconImageViewSize = DeletedMessageView.iconImageViewSize
imageView.set(.width, to: iconImageViewSize)
imageView.set(.height, to: iconImageViewSize)
// Body label
let titleLabel = UILabel()
titleLabel.lineBreakMode = .byTruncatingTail
titleLabel.text = NSLocalizedString("message_deleted", comment: "")
titleLabel.textColor = textColor
titleLabel.font = .systemFont(ofSize: Values.smallFontSize)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 6)
addSubview(stackView)
stackView.pin(to: self, withInset: Values.smallSpacing)
}
}

View File

@ -202,7 +202,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
doubleTapGestureRecognizer.numberOfTapsRequired = 2
addGestureRecognizer(doubleTapGestureRecognizer)
tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer)
addGestureRecognizer(panGestureRecognizer)
}
// MARK: Updating
@ -275,6 +274,12 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
timerView.isHidden = !viewItem.isExpiringMessage
timerViewOutgoingMessageConstraint.isActive = (direction == .outgoing)
timerViewIncomingMessageConstraint.isActive = (direction == .incoming)
// Swipe to reply
if (message.isDeleted) {
removeGestureRecognizer(panGestureRecognizer)
} else {
addGestureRecognizer(panGestureRecognizer)
}
}
private func populateHeader(for viewItem: ConversationViewItem) {
@ -389,6 +394,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
snContentView.addSubview(documentView)
documentView.pin(to: snContentView)
}
case .deletedMessage:
let deletedMessageView = DeletedMessageView(viewItem: viewItem, textColor: bodyLabelTextColor)
snContentView.addSubview(deletedMessageView)
deletedMessageView.pin(to: snContentView)
default: return
}
}

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,12 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -549,3 +549,11 @@
"vc_conversation_settings_notify_for_mentions_only_title" = "Notify for Mentions Only";
"vc_conversation_settings_notify_for_mentions_only_explanation" = "When enabled, you'll only be notified for messages mentioning you.";
"view_conversation_title_notify_for_mentions_only" = "Notifying for Mentions Only";
"message_deleted" = "This message has been deleted";
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
"context_menu_ban_and_delete_all" = "Ban and Delete All";

View File

@ -95,6 +95,7 @@ protocol NotificationPresenterAdaptee: class {
func notify(category: AppNotificationCategory, title: String?, body: String, userInfo: [AnyHashable: Any], sound: OWSSound?, replacingIdentifier: String?)
func cancelNotifications(threadId: String)
func cancelNotification(identifier: String)
func clearAllNotifications()
}
@ -219,6 +220,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
let userInfo = [
AppNotificationUserInfoKey.threadId: threadId
]
let identifier: String = incomingMessage.notificationIdentifier ?? UUID().uuidString
DispatchQueue.main.async {
notificationBody = MentionUtilities.highlightMentions(in: notificationBody!, threadID: thread.uniqueId!)
@ -227,7 +230,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
title: notificationTitle,
body: notificationBody ?? "",
userInfo: userInfo,
sound: sound)
sound: sound,
replacingIdentifier: identifier)
}
}
@ -260,6 +264,13 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
sound: sound)
}
}
@objc
public func cancelNotification(_ identifier: String) {
DispatchQueue.main.async {
self.adaptee.cancelNotification(identifier: identifier)
}
}
@objc
public func cancelNotifications(threadId: String) {

View File

@ -48,7 +48,7 @@ public final class BackgroundPoller : NSObject {
// messages failed to parse.
guard let envelope = SNProtoEnvelope.from(json),
let data = try? envelope.serializedData() else { return nil }
let job = MessageReceiveJob(data: data, isBackgroundPoll: true)
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: true)
return job.execute()
}
return when(fulfilled: promises) // The promise returned by MessageReceiveJob never rejects

View File

@ -3,6 +3,7 @@ import PromiseKit
public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public let data: Data
public let serverHash: String?
public let openGroupMessageServerID: UInt64?
public let openGroupID: String?
public let isBackgroundPoll: Bool
@ -15,8 +16,9 @@ public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSC
public static let maxFailureCount: UInt = 10
// MARK: Initialization
public init(data: Data, openGroupMessageServerID: UInt64? = nil, openGroupID: String? = nil, isBackgroundPoll: Bool) {
public init(data: Data, serverHash: String? = nil, openGroupMessageServerID: UInt64? = nil, openGroupID: String? = nil, isBackgroundPoll: Bool) {
self.data = data
self.serverHash = serverHash
self.openGroupMessageServerID = openGroupMessageServerID
self.openGroupID = openGroupID
self.isBackgroundPoll = isBackgroundPoll
@ -32,6 +34,7 @@ public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSC
let id = coder.decodeObject(forKey: "id") as! String?,
let isBackgroundPoll = coder.decodeObject(forKey: "isBackgroundPoll") as! Bool? else { return nil }
self.data = data
self.serverHash = coder.decodeObject(forKey: "serverHash") as! String?
self.openGroupMessageServerID = coder.decodeObject(forKey: "openGroupMessageServerID") as! UInt64?
self.openGroupID = coder.decodeObject(forKey: "openGroupID") as! String?
self.isBackgroundPoll = isBackgroundPoll
@ -41,6 +44,7 @@ public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSC
public func encode(with coder: NSCoder) {
coder.encode(data, forKey: "data")
coder.encode(serverHash, forKey: "serverHash")
coder.encode(openGroupMessageServerID, forKey: "openGroupMessageServerID")
coder.encode(openGroupID, forKey: "openGroupID")
coder.encode(isBackgroundPoll, forKey: "isBackgroundPoll")
@ -62,6 +66,7 @@ public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSC
do {
let isRetry = (self.failureCount != 0)
let (message, proto) = try MessageReceiver.parse(self.data, openGroupMessageServerID: self.openGroupMessageServerID, isRetry: isRetry, using: transaction)
message.serverHash = self.serverHash
try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: self.openGroupID, isBackgroundPoll: self.isBackgroundPoll, using: transaction)
self.handleSuccess()
seal.fulfill(())

View File

@ -0,0 +1,71 @@
import SessionUtilitiesKit
@objc(SNUnsendRequest)
public final class UnsendRequest: ControlMessage {
public var timestamp: UInt64?
public var author: String?
public override var isSelfSendValid: Bool { true }
// MARK: Validation
public override var isValid: Bool {
guard super.isValid else { return false }
return timestamp != nil && author != nil
}
// MARK: Initialization
public override init() { super.init() }
internal init(timestamp: UInt64, author: String) {
super.init()
self.timestamp = timestamp
self.author = author
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
if let timestamp = coder.decodeObject(forKey: "timestamp") as! UInt64? { self.timestamp = timestamp }
if let author = coder.decodeObject(forKey: "author") as! String? { self.author = author }
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(timestamp, forKey: "timestamp")
coder.encode(author, forKey: "author")
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> UnsendRequest? {
guard let unsendRequestProto = proto.unsendRequest else { return nil }
let timestamp = unsendRequestProto.timestamp
let author = unsendRequestProto.author
return UnsendRequest(timestamp: timestamp, author: author)
}
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
guard let timestamp = timestamp, let author = author else {
SNLog("Couldn't construct unsend request proto from: \(self).")
return nil
}
let unsendRequestProto = SNProtoUnsendRequest.builder(timestamp: timestamp, author: author)
let contentProto = SNProtoContent.builder()
do {
contentProto.setUnsendRequest(try unsendRequestProto.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct unsend request proto from: \(self).")
return nil
}
}
// MARK: Description
public override var description: String {
"""
UnsendRequest(
timestamp: \(timestamp?.description ?? "null")
author: \(author?.description ?? "null")
)
"""
}
}

View File

@ -11,6 +11,7 @@ public class Message : NSObject, NSCoding { // NSObject/NSCoding conformance is
public var groupPublicKey: String?
public var openGroupServerMessageID: UInt64?
public var openGroupServerTimestamp: UInt64?
public var serverHash: String?
public var ttl: UInt64 { 14 * 24 * 60 * 60 * 1000 }
public var isSelfSendValid: Bool { false }
@ -35,6 +36,7 @@ public class Message : NSObject, NSCoding { // NSObject/NSCoding conformance is
if let groupPublicKey = coder.decodeObject(forKey: "groupPublicKey") as! String? { self.groupPublicKey = groupPublicKey }
if let openGroupServerMessageID = coder.decodeObject(forKey: "openGroupServerMessageID") as! UInt64? { self.openGroupServerMessageID = openGroupServerMessageID }
if let openGroupServerTimestamp = coder.decodeObject(forKey: "openGroupServerTimestamp") as! UInt64? { self.openGroupServerTimestamp = openGroupServerTimestamp }
if let serverHash = coder.decodeObject(forKey: "serverHash") as! String? { self.serverHash = serverHash }
}
public func encode(with coder: NSCoder) {
@ -47,6 +49,7 @@ public class Message : NSObject, NSCoding { // NSObject/NSCoding conformance is
coder.encode(groupPublicKey, forKey: "groupPublicKey")
coder.encode(openGroupServerMessageID, forKey: "openGroupServerMessageID")
coder.encode(openGroupServerTimestamp, forKey: "openGroupServerTimestamp")
coder.encode(serverHash, forKey: "serverHash")
}
// MARK: Proto Conversion

View File

@ -22,7 +22,8 @@ public extension TSIncomingMessage {
serverTimestamp: visibleMessage.openGroupServerTimestamp as NSNumber?,
wasReceivedByUD: true,
openGroupInvitationName: visibleMessage.openGroupInvitation?.name,
openGroupInvitationURL: visibleMessage.openGroupInvitation?.url
openGroupInvitationURL: visibleMessage.openGroupInvitation?.url,
serverHash: visibleMessage.serverHash
)
result.openGroupServerMessageID = openGroupServerMessageID
return result

View File

@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) BOOL isUserMentioned;
@property (nonatomic, readonly, nullable) NSString *notificationIdentifier;
- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body
@ -62,7 +64,8 @@ NS_ASSUME_NONNULL_BEGIN
serverTimestamp:(nullable NSNumber *)serverTimestamp
wasReceivedByUD:(BOOL)wasReceivedByUD
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL NS_DESIGNATED_INITIALIZER;
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
serverHash:(nullable NSString*)serverHash NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@ -88,6 +91,9 @@ NS_ASSUME_NONNULL_BEGIN
- (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)setNotificationIdentifier:(NSString * _Nullable)notificationIdentifier
transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end
NS_ASSUME_NONNULL_END

View File

@ -56,6 +56,7 @@ NS_ASSUME_NONNULL_BEGIN
wasReceivedByUD:(BOOL)wasReceivedByUD
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
serverHash:(nullable NSString *)serverHash
{
self = [super initMessageWithTimestamp:timestamp
inThread:thread
@ -66,7 +67,8 @@ NS_ASSUME_NONNULL_BEGIN
quotedMessage:quotedMessage
linkPreview:linkPreview
openGroupInvitationName:openGroupInvitationName
openGroupInvitationURL:openGroupInvitationURL];
openGroupInvitationURL:openGroupInvitationURL
serverHash:serverHash];
if (!self) {
return self;
@ -77,6 +79,7 @@ NS_ASSUME_NONNULL_BEGIN
_read = NO;
_serverTimestamp = serverTimestamp;
_wasReceivedByUD = wasReceivedByUD;
_notificationIdentifier = nil;
return self;
}
@ -128,6 +131,12 @@ NS_ASSUME_NONNULL_BEGIN
return (self.body != nil && [self.body containsString:[NSString stringWithFormat:@"@%@", userPublicKey]]) || (self.quotedMessage != nil && [self.quotedMessage.authorId isEqualToString:userPublicKey]);
}
- (void)setNotificationIdentifier:(NSString * _Nullable)notificationIdentifier transaction:(nonnull YapDatabaseReadWriteTransaction *)transaction
{
_notificationIdentifier = notificationIdentifier;
[self saveWithTransaction:transaction];
}
#pragma mark - OWSReadTracking
- (BOOL)shouldAffectUnreadCounts

View File

@ -57,7 +57,8 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
quotedMessage:nil
linkPreview:nil
openGroupInvitationName:nil
openGroupInvitationURL:nil];
openGroupInvitationURL:nil
serverHash:nil];
if (!self) {
return self;

View File

@ -38,6 +38,8 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold;
@property (nonatomic, readonly) BOOL isOpenGroupMessage;
@property (nonatomic, readonly, nullable) NSString *openGroupInvitationName;
@property (nonatomic, readonly, nullable) NSString *openGroupInvitationURL;
@property (nonatomic, nullable) NSString *serverHash;
@property (nonatomic) BOOL isDeleted;
- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE;
@ -50,7 +52,8 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold;
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL NS_DESIGNATED_INITIALIZER;
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
serverHash:(nullable NSString *)serverHash NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@ -80,6 +83,8 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold;
- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateForDeletionWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
@end
NS_ASSUME_NONNULL_END

View File

@ -65,6 +65,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
linkPreview:(nullable OWSLinkPreview *)linkPreview
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
serverHash:(nullable NSString *)serverHash
{
self = [super initInteractionWithTimestamp:timestamp inThread:thread];
@ -84,6 +85,8 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
_openGroupServerMessageID = 0;
_openGroupInvitationName = openGroupInvitationName;
_openGroupInvitationURL = openGroupInvitationURL;
_serverHash = serverHash;
_isDeleted = false;
return self;
}
@ -421,6 +424,23 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
}];
}
- (void)updateForDeletionWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSMessage *message) {
[message setBody:nil];
[message setServerHash:nil];
for (NSString *attachmentId in message.attachmentIds) {
TSAttachment *_Nullable attachment =
[TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
if (attachment) {
[attachment removeWithTransaction:transaction];
}
}
[message setIsDeleted:true];
}];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -30,7 +30,8 @@ import SessionUtilitiesKit
quotedMessage: TSQuotedMessage.from(visibleMessage.quote),
linkPreview: OWSLinkPreview.from(visibleMessage.linkPreview),
openGroupInvitationName: visibleMessage.openGroupInvitation?.name,
openGroupInvitationURL: visibleMessage.openGroupInvitation?.url
openGroupInvitationURL: visibleMessage.openGroupInvitation?.url,
serverHash: visibleMessage.serverHash
)
}
}

View File

@ -94,7 +94,8 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
linkPreview:(nullable OWSLinkPreview *)linkPreview
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL NS_DESIGNATED_INITIALIZER;
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
serverHash:(nullable NSString *)serverHash NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

View File

@ -154,7 +154,8 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
quotedMessage:quotedMessage
linkPreview:linkPreview
openGroupInvitationName:nil
openGroupInvitationURL:nil];
openGroupInvitationURL:nil
serverHash:nil];
}
+ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread
@ -173,7 +174,8 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
quotedMessage:nil
linkPreview:nil
openGroupInvitationName:nil
openGroupInvitationURL:nil];
openGroupInvitationURL:nil
serverHash:nil];
}
- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp
@ -188,6 +190,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
linkPreview:(nullable OWSLinkPreview *)linkPreview
openGroupInvitationName:(nullable NSString *)openGroupInvitationName
openGroupInvitationURL:(nullable NSString *)openGroupInvitationURL
serverHash:(nullable NSString *)serverHash
{
self = [super initMessageWithTimestamp:timestamp
inThread:thread
@ -198,7 +201,8 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
quotedMessage:quotedMessage
linkPreview:linkPreview
openGroupInvitationName:openGroupInvitationName
openGroupInvitationURL:openGroupInvitationURL];
openGroupInvitationURL:openGroupInvitationURL
serverHash:serverHash];
if (!self) {
return self;
}
@ -559,6 +563,14 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
}];
}
#pragma mark - Delete
- (void)updateForDeletionWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super updateForDeletionWithTransaction:transaction];
[self removeWithTransaction:transaction];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -338,6 +338,118 @@ extension SNProtoTypingMessage.SNProtoTypingMessageBuilder {
#endif
// MARK: - SNProtoUnsendRequest
@objc public class SNProtoUnsendRequest: NSObject {
// MARK: - SNProtoUnsendRequestBuilder
@objc public class func builder(timestamp: UInt64, author: String) -> SNProtoUnsendRequestBuilder {
return SNProtoUnsendRequestBuilder(timestamp: timestamp, author: author)
}
// asBuilder() constructs a builder that reflects the proto's contents.
@objc public func asBuilder() -> SNProtoUnsendRequestBuilder {
let builder = SNProtoUnsendRequestBuilder(timestamp: timestamp, author: author)
return builder
}
@objc public class SNProtoUnsendRequestBuilder: NSObject {
private var proto = SessionProtos_UnsendRequest()
@objc fileprivate override init() {}
@objc fileprivate init(timestamp: UInt64, author: String) {
super.init()
setTimestamp(timestamp)
setAuthor(author)
}
@objc public func setTimestamp(_ valueParam: UInt64) {
proto.timestamp = valueParam
}
@objc public func setAuthor(_ valueParam: String) {
proto.author = valueParam
}
@objc public func build() throws -> SNProtoUnsendRequest {
return try SNProtoUnsendRequest.parseProto(proto)
}
@objc public func buildSerializedData() throws -> Data {
return try SNProtoUnsendRequest.parseProto(proto).serializedData()
}
}
fileprivate let proto: SessionProtos_UnsendRequest
@objc public let timestamp: UInt64
@objc public let author: String
private init(proto: SessionProtos_UnsendRequest,
timestamp: UInt64,
author: String) {
self.proto = proto
self.timestamp = timestamp
self.author = author
}
@objc
public func serializedData() throws -> Data {
return try self.proto.serializedData()
}
@objc public class func parseData(_ serializedData: Data) throws -> SNProtoUnsendRequest {
let proto = try SessionProtos_UnsendRequest(serializedData: serializedData)
return try parseProto(proto)
}
fileprivate class func parseProto(_ proto: SessionProtos_UnsendRequest) throws -> SNProtoUnsendRequest {
guard proto.hasTimestamp else {
throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: timestamp")
}
let timestamp = proto.timestamp
guard proto.hasAuthor else {
throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: author")
}
let author = proto.author
// MARK: - Begin Validation Logic for SNProtoUnsendRequest -
// MARK: - End Validation Logic for SNProtoUnsendRequest -
let result = SNProtoUnsendRequest(proto: proto,
timestamp: timestamp,
author: author)
return result
}
@objc public override var debugDescription: String {
return "\(proto)"
}
}
#if DEBUG
extension SNProtoUnsendRequest {
@objc public func serializedDataIgnoringErrors() -> Data? {
return try! self.serializedData()
}
}
extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
@objc public func buildIgnoringErrors() -> SNProtoUnsendRequest? {
return try! self.build()
}
}
#endif
// MARK: - SNProtoContent
@objc public class SNProtoContent: NSObject {
@ -366,6 +478,9 @@ extension SNProtoTypingMessage.SNProtoTypingMessageBuilder {
if let _value = dataExtractionNotification {
builder.setDataExtractionNotification(_value)
}
if let _value = unsendRequest {
builder.setUnsendRequest(_value)
}
return builder
}
@ -395,6 +510,10 @@ extension SNProtoTypingMessage.SNProtoTypingMessageBuilder {
proto.dataExtractionNotification = valueParam.proto
}
@objc public func setUnsendRequest(_ valueParam: SNProtoUnsendRequest) {
proto.unsendRequest = valueParam.proto
}
@objc public func build() throws -> SNProtoContent {
return try SNProtoContent.parseProto(proto)
}
@ -416,18 +535,22 @@ extension SNProtoTypingMessage.SNProtoTypingMessageBuilder {
@objc public let dataExtractionNotification: SNProtoDataExtractionNotification?
@objc public let unsendRequest: SNProtoUnsendRequest?
private init(proto: SessionProtos_Content,
dataMessage: SNProtoDataMessage?,
receiptMessage: SNProtoReceiptMessage?,
typingMessage: SNProtoTypingMessage?,
configurationMessage: SNProtoConfigurationMessage?,
dataExtractionNotification: SNProtoDataExtractionNotification?) {
dataExtractionNotification: SNProtoDataExtractionNotification?,
unsendRequest: SNProtoUnsendRequest?) {
self.proto = proto
self.dataMessage = dataMessage
self.receiptMessage = receiptMessage
self.typingMessage = typingMessage
self.configurationMessage = configurationMessage
self.dataExtractionNotification = dataExtractionNotification
self.unsendRequest = unsendRequest
}
@objc
@ -466,6 +589,11 @@ extension SNProtoTypingMessage.SNProtoTypingMessageBuilder {
dataExtractionNotification = try SNProtoDataExtractionNotification.parseProto(proto.dataExtractionNotification)
}
var unsendRequest: SNProtoUnsendRequest? = nil
if proto.hasUnsendRequest {
unsendRequest = try SNProtoUnsendRequest.parseProto(proto.unsendRequest)
}
// MARK: - Begin Validation Logic for SNProtoContent -
// MARK: - End Validation Logic for SNProtoContent -
@ -475,7 +603,8 @@ extension SNProtoTypingMessage.SNProtoTypingMessageBuilder {
receiptMessage: receiptMessage,
typingMessage: typingMessage,
configurationMessage: configurationMessage,
dataExtractionNotification: dataExtractionNotification)
dataExtractionNotification: dataExtractionNotification,
unsendRequest: unsendRequest)
return result
}

View File

@ -196,6 +196,39 @@ extension SessionProtos_TypingMessage.Action: CaseIterable {
#endif // swift(>=4.2)
struct SessionProtos_UnsendRequest {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// @required
var timestamp: UInt64 {
get {return _timestamp ?? 0}
set {_timestamp = newValue}
}
/// Returns true if `timestamp` has been explicitly set.
var hasTimestamp: Bool {return self._timestamp != nil}
/// Clears the value of `timestamp`. Subsequent reads from it will return its default value.
mutating func clearTimestamp() {self._timestamp = nil}
/// @required
var author: String {
get {return _author ?? String()}
set {_author = newValue}
}
/// Returns true if `author` has been explicitly set.
var hasAuthor: Bool {return self._author != nil}
/// Clears the value of `author`. Subsequent reads from it will return its default value.
mutating func clearAuthor() {self._author = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _timestamp: UInt64? = nil
fileprivate var _author: String? = nil
}
struct SessionProtos_Content {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
@ -246,6 +279,15 @@ struct SessionProtos_Content {
/// Clears the value of `dataExtractionNotification`. Subsequent reads from it will return its default value.
mutating func clearDataExtractionNotification() {self._dataExtractionNotification = nil}
var unsendRequest: SessionProtos_UnsendRequest {
get {return _unsendRequest ?? SessionProtos_UnsendRequest()}
set {_unsendRequest = newValue}
}
/// Returns true if `unsendRequest` has been explicitly set.
var hasUnsendRequest: Bool {return self._unsendRequest != nil}
/// Clears the value of `unsendRequest`. Subsequent reads from it will return its default value.
mutating func clearUnsendRequest() {self._unsendRequest = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
@ -255,6 +297,7 @@ struct SessionProtos_Content {
fileprivate var _typingMessage: SessionProtos_TypingMessage? = nil
fileprivate var _configurationMessage: SessionProtos_ConfigurationMessage? = nil
fileprivate var _dataExtractionNotification: SessionProtos_DataExtractionNotification? = nil
fileprivate var _unsendRequest: SessionProtos_UnsendRequest? = nil
}
struct SessionProtos_KeyPair {
@ -1504,6 +1547,50 @@ extension SessionProtos_TypingMessage.Action: SwiftProtobuf._ProtoNameProviding
]
}
extension SessionProtos_UnsendRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".UnsendRequest"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "timestamp"),
2: .same(proto: "author"),
]
public var isInitialized: Bool {
if self._timestamp == nil {return false}
if self._author == nil {return false}
return true
}
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt64Field(value: &self._timestamp) }()
case 2: try { try decoder.decodeSingularStringField(value: &self._author) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._timestamp {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
}
if let v = self._author {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SessionProtos_UnsendRequest, rhs: SessionProtos_UnsendRequest) -> Bool {
if lhs._timestamp != rhs._timestamp {return false}
if lhs._author != rhs._author {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".Content"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@ -1512,6 +1599,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
6: .same(proto: "typingMessage"),
7: .same(proto: "configurationMessage"),
8: .same(proto: "dataExtractionNotification"),
9: .same(proto: "unsendRequest"),
]
public var isInitialized: Bool {
@ -1520,6 +1608,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
if let v = self._typingMessage, !v.isInitialized {return false}
if let v = self._configurationMessage, !v.isInitialized {return false}
if let v = self._dataExtractionNotification, !v.isInitialized {return false}
if let v = self._unsendRequest, !v.isInitialized {return false}
return true
}
@ -1534,6 +1623,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
case 6: try { try decoder.decodeSingularMessageField(value: &self._typingMessage) }()
case 7: try { try decoder.decodeSingularMessageField(value: &self._configurationMessage) }()
case 8: try { try decoder.decodeSingularMessageField(value: &self._dataExtractionNotification) }()
case 9: try { try decoder.decodeSingularMessageField(value: &self._unsendRequest) }()
default: break
}
}
@ -1555,6 +1645,9 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
if let v = self._dataExtractionNotification {
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
}
if let v = self._unsendRequest {
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1564,6 +1657,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
if lhs._typingMessage != rhs._typingMessage {return false}
if lhs._configurationMessage != rhs._configurationMessage {return false}
if lhs._dataExtractionNotification != rhs._dataExtractionNotification {return false}
if lhs._unsendRequest != rhs._unsendRequest {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View File

@ -34,12 +34,20 @@ message TypingMessage {
required Action action = 2;
}
message UnsendRequest {
// @required
required uint64 timestamp = 1;
// @required
required string author = 2;
}
message Content {
optional DataMessage dataMessage = 1;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional ConfigurationMessage configurationMessage = 7;
optional DataExtractionNotification dataExtractionNotification = 8;
optional UnsendRequest unsendRequest = 9;
}
message KeyPair {

View File

@ -1,4 +1,5 @@
import SignalCoreKit
import SessionSnodeKit
extension MessageReceiver {
@ -14,6 +15,7 @@ extension MessageReceiver {
case let message as DataExtractionNotification: handleDataExtractionNotification(message, using: transaction)
case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction)
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
case let message as UnsendRequest: handleUnsendRequest(message, using: transaction)
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
default: fatalError()
}
@ -218,6 +220,34 @@ extension MessageReceiver {
// MARK: - Unsend Requests
public static func handleUnsendRequest(_ message: UnsendRequest, using transaction: Any) {
guard message.sender == message.author else { return }
let userPublicKey = getUserHexEncodedPublicKey()
let transaction = transaction as! YapDatabaseReadWriteTransaction
if let author = message.author, let timestamp = message.timestamp {
let localMessage: TSMessage?
if userPublicKey == message.sender { localMessage = TSOutgoingMessage.find(withTimestamp: timestamp) }
else { localMessage = TSIncomingMessage.find(withAuthorId: author, timestamp: timestamp, transaction: transaction) }
if let messageToDelete = localMessage {
if let incomingMessage = messageToDelete as? TSIncomingMessage {
incomingMessage.markAsReadNow(withSendReadReceipt: false, transaction: transaction)
if let notificationIdentifier = incomingMessage.notificationIdentifier, !notificationIdentifier.isEmpty {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationIdentifier])
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notificationIdentifier])
}
}
if let serverHash = messageToDelete.serverHash {
SnodeAPI.deleteMessage(publicKey: author, serverHashes: [serverHash]).retainUntilComplete()
}
messageToDelete.updateForDeletion(with: transaction)
}
}
}
// MARK: - Visible Messages
@discardableResult
@ -288,6 +318,7 @@ extension MessageReceiver {
// Notify the user if needed
guard (isMainAppAndActive || isBackgroundPoll), let tsIncomingMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) as? TSIncomingMessage,
let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return tsMessageID }
tsIncomingMessage.setNotificationIdentifier(UUID().uuidString, transaction: transaction)
DispatchQueue.main.async {
Storage.read { transaction in
SSKEnvironment.shared.notificationsManager!.notifyUser(for: tsIncomingMessage, in: thread, transaction: transaction)

View File

@ -124,6 +124,7 @@ public enum MessageReceiver {
if let dataExtractionNotification = DataExtractionNotification.fromProto(proto) { return dataExtractionNotification }
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto) { return expirationTimerUpdate }
if let configurationMessage = ConfigurationMessage.fromProto(proto) { return configurationMessage }
if let unsendRequest = UnsendRequest.fromProto(proto) { return unsendRequest }
if let visibleMessage = VisibleMessage.fromProto(proto) { return visibleMessage }
return nil
}()

View File

@ -134,8 +134,9 @@ public final class MessageSender : NSObject {
// a configuration message
// a sync message
// a closed group control message of type `new`
// an unsend request
let isNewClosedGroupControlMessage = given(message as? ClosedGroupControlMessage) { if case .new = $0.kind { return true } else { return false } } ?? false
guard !isSelfSend || message is ConfigurationMessage || isSyncMessage || isNewClosedGroupControlMessage else {
guard !isSelfSend || message is ConfigurationMessage || isSyncMessage || isNewClosedGroupControlMessage || message is UnsendRequest else {
storage.write(with: { transaction in
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
seal.fulfill(())
@ -207,12 +208,15 @@ public final class MessageSender : NSObject {
let promiseCount = promises.count
var errorCount = 0
promises.forEach {
let _ = $0.done(on: DispatchQueue.global(qos: .userInitiated)) { _ in
let _ = $0.done(on: DispatchQueue.global(qos: .userInitiated)) { rawResponse in
guard !isSuccess else { return } // Succeed as soon as the first promise succeeds
isSuccess = true
storage.write(with: { transaction in
let json = rawResponse as? JSON
let hash = json?["hash"] as? String
message.serverHash = hash
MessageSender.handleSuccessfulMessageSend(message, to: destination, isSyncMessage: isSyncMessage, using: transaction)
var shouldNotify = (message is VisibleMessage && !isSyncMessage)
var shouldNotify = ((message is VisibleMessage || message is UnsendRequest) && !isSyncMessage)
/*
if let closedGroupControlMessage = message as? ClosedGroupControlMessage, case .new = closedGroupControlMessage.kind {
shouldNotify = true
@ -328,6 +332,10 @@ public final class MessageSender : NSObject {
Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp!, using: transaction)
// Get the visible message if possible
if let tsMessage = TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) {
// When the sync message is successfully sent, the hash value of this TSOutgoingMessage
// will be replaced by the hash value of the sync message. Since the hash value of the
// real message has no use when we delete a message. It is OK to let it be.
tsMessage.serverHash = message.serverHash
// Track the open group server message ID
tsMessage.openGroupServerMessageID = message.openGroupServerMessageID ?? 0
// Mark the message as sent

View File

@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
inThread:(TSThread *)thread
transaction:(YapDatabaseReadTransaction *)transaction;
- (void)cancelNotification:(NSString *)identifier;
- (void)clearAllNotifications;
@end

View File

@ -117,7 +117,7 @@ public final class ClosedGroupPoller : NSObject {
guard let envelope = SNProtoEnvelope.from(json) else { return }
do {
let data = try envelope.serializedData()
let job = MessageReceiveJob(data: data, isBackgroundPoll: false)
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: false)
SNMessagingKitConfiguration.shared.storage.write { transaction in
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
}

View File

@ -97,7 +97,7 @@ public final class Poller : NSObject {
guard let envelope = SNProtoEnvelope.from(json) else { return }
do {
let data = try envelope.serializedData()
let job = MessageReceiveJob(data: data, isBackgroundPoll: false)
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: false)
SNMessagingKitConfiguration.shared.storage.write { transaction in
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
}

View File

@ -314,6 +314,13 @@ BOOL IsNoteToSelfEnabled(void)
if (interaction.isDynamicInteraction) {
return NO;
}
if ([interaction isKindOfClass:[TSMessage class]]) {
TSMessage *message = (TSMessage *)interaction;
if (message.isDeleted) {
return NO;
}
}
return YES;
}

View File

@ -67,6 +67,11 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
return self.completeSilenty()
}
}
// Store the notification identifier for unsend request to cancel this notification
tsIncomingMessage.setNotificationIdentifier(request.identifier, transaction: transaction)
case let unsendRequest as UnsendRequest:
MessageReceiver.handleUnsendRequest(unsendRequest, using: transaction)
return self.completeSilenty()
case let closedGroupControlMessage as ClosedGroupControlMessage:
// TODO: We could consider actually handling the update here. Not sure if there's enough time though, seeing as though
// in some cases we need to send messages (e.g. our sender key) to a number of other users.

View File

@ -14,6 +14,7 @@ public final class Snode : NSObject, NSCoding { // NSObject/NSCoding conformance
case getSwarm = "get_snodes_for_pubkey"
case getMessages = "retrieve"
case sendMessage = "store"
case deleteMessage = "delete"
case oxenDaemonRPCCall = "oxend_request"
case getInfo = "info"
case clearAllData = "delete_all"

View File

@ -454,6 +454,56 @@ public final class SnodeAPI : NSObject {
return promise
}
@objc(deleteMessageForPublickKey:serverHashes:)
public static func objc_deleteMessage(publicKey: String, serverHashes: [String]) -> AnyPromise {
AnyPromise.from(deleteMessage(publicKey: publicKey, serverHashes: serverHashes))
}
public static func deleteMessage(publicKey: String, serverHashes: [String]) -> Promise<[String:Bool]> {
let storage = SNSnodeKitConfiguration.shared.storage
guard let userX25519PublicKey = storage.getUserPublicKey(),
let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
let publicKey = Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
getSwarm(for: publicKey).then2 { swarm -> Promise<[String:Bool]> in
let snode = swarm.randomElement()!
let verificationData = (Snode.Method.deleteMessage.rawValue + serverHashes.joined(separator: "")).data(using: String.Encoding.utf8)!
guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { throw Error.signingFailed }
let parameters: JSON = [
"pubkey" : userX25519PublicKey,
"pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(),
"messages": serverHashes,
"signature": signature.toBase64()!
]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters).map2{ rawResponse -> [String:Bool] in
guard let json = rawResponse as? JSON, let swarm = json["swarm"] as? JSON else { throw HTTP.Error.invalidJSON }
var result: [String:Bool] = [:]
for (snodePublicKey, rawJSON) in swarm {
guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON }
let isFailed = json["failed"] as? Bool ?? false
if !isFailed {
guard let hashes = json["deleted"] as? [String], let signature = json["signature"] as? String else { throw HTTP.Error.invalidJSON }
// The signature format is ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
let verificationData = (userX25519PublicKey + serverHashes.joined(separator: "") + hashes.joined(separator: "")).data(using: String.Encoding.utf8)!
let isValid = sodium.sign.verify(message: Bytes(verificationData), publicKey: Bytes(Data(hex: snodePublicKey)), signature: Bytes(Data(base64Encoded: signature)!))
result[snodePublicKey] = isValid
} else {
if let reason = json["reason"] as? String, let statusCode = json["code"] as? String {
SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).")
} else {
SNLog("Couldn't delete data from: \(snodePublicKey).")
}
result[snodePublicKey] = false
}
}
return result
}
}
}
}
}
/// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation.
public static func clearAllData() -> Promise<[String:Bool]> {
let storage = SNSnodeKitConfiguration.shared.storage

View File

@ -8,6 +8,10 @@ public class NoopNotificationsManager: NSObject, NotificationsProtocol {
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
owsFailDebug("")
}
public func cancelNotification(_ identifier: String) {
owsFailDebug("")
}
public func clearAllNotifications() {
Logger.warn("clearAllNotifications")