Fixed a number of reported bugs, some cleanup, added animated profile support

Added support for animated profile images (no ability to crop/resize)
Updated the message trimming to only remove messages if the open group has 2000 messages or more
Updated the message trimming setting to default to be on
Updated the ContextMenu to fade out the snapshot as well (looked buggy if the device had even minor lag)
Updated the ProfileManager to delete and re-download invalid avatar images (and updated the conversation screen to reload when avatars complete downloading)
Updated the message request notification logic so it will show notifications when receiving a new message request as long as the user has read all the old ones (previously the user had to accept/reject all the old ones)
Fixed a bug where the "trim open group messages" toggle was accessing UI off the main thread
Fixed a bug where the "Chats" settings screen had a close button instead of a back button
Fixed a bug where the 'viewsToMove' for the reply UI was inconsistent in some places
Fixed an issue where the ProfileManager was doing all of it's validation (and writing to disk) within the database write closure which would block database writes unnecessarily
Fixed a bug where a message request wouldn't be identified as such just because it wasn't visible in the conversations list
Fixed a bug where opening a message request notification would result in the message request being in the wrong state (also wouldn't insert the 'MessageRequestsViewController' into the hierarchy)
Fixed a bug where the avatar image wouldn't appear beside incoming closed group message in some situations cases
Removed an error log that was essentially just spam
Remove the logic to delete old profile images when calling save on a Profile (wouldn't get called if the row was modified directly and duplicates GarbageCollection logic)
Remove the logic to send a notification when calling save on a Profile (wouldn't get called if the row was modified directly)
Tweaked the message trimming description to be more accurate
Cleaned up some duplicate logic used to determine if a notification should be shown
Cleaned up some onion request logic (was passing the version info in some cases when not needed)
Moved the push notification notify API call into the PushNotificationAPI class for consistency
This commit is contained in:
Morgan Pretty 2022-07-08 17:53:48 +10:00
parent 6b9a19c761
commit 4afddd6fbb
58 changed files with 500 additions and 412 deletions

View File

@ -261,7 +261,6 @@
B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DE1FB526C22FCB0079C9CE /* CallMessage.swift */; };
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */; };
B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */; };
B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */; };
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */; };
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */; };
B8F5F71A25F1B35C003BF8D4 /* MediaPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */; };
@ -1296,7 +1295,6 @@
B8EB20E6263F7E4B00773E52 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+OpenGroupInvitation.swift"; sourceTree = "<group>"; };
B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView.swift; sourceTree = "<group>"; };
B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Contacts.swift"; sourceTree = "<group>"; };
B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Subscripting.swift"; sourceTree = "<group>"; };
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotification.swift; sourceTree = "<group>"; };
B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaceholderView.swift; sourceTree = "<group>"; };
@ -2630,7 +2628,6 @@
FD17D79A27F40ADA00122BE0 /* LegacyDatabase */,
FD17D79427F3E03300122BE0 /* Migrations */,
FD09796C27FA6C8B00936362 /* Models */,
B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */,
);
path = Database;
sourceTree = "<group>";
@ -5176,7 +5173,6 @@
FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */,
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */,
FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */,
B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */,
FD09796E27FA6D0000936362 /* Contact.swift in Sources */,
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */,
FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */,

View File

@ -152,6 +152,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
self.contactName = Profile.displayName(db, id: sessionId, threadVariant: .contact)
self.profilePicture = ProfileManager.profileAvatar(db, id: sessionId)
.map { UIImage(data: $0) }
.defaulting(to: Identicon.generatePlaceholderIcon(seed: sessionId, text: self.contactName, size: 300))
WebRTCSession.current = self.webRTCSession

View File

@ -174,6 +174,7 @@ final class ContextMenuVC: UIViewController {
animations: { [weak self] in
self?.blurView.effect = nil
self?.menuView.alpha = 0
self?.snapshot.alpha = 0
self?.timestampLabel.alpha = 0
},
completion: { [weak self] _ in

View File

@ -185,6 +185,16 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
return SQL("LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(interaction[.threadId])")
}()
),
PagedData.ObservedChanges(
table: Profile.self,
columns: [.profilePictureFileName],
joinToPagedType: {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias()
return SQL("LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])")
}()
)
],
filterSQL: MessageViewModel.filterSQL(threadId: threadId),

View File

@ -38,6 +38,15 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel
// MARK: - UI Components
private lazy var viewsToMoveForReply: [UIView] = [
bubbleView,
bubbleBackgroundView,
profilePictureView,
replyButton,
timerView,
messageStatusImageView
]
private lazy var profilePictureView: ProfilePictureView = {
let result: ProfilePictureView = ProfilePictureView()
result.set(.height, to: Values.verySmallProfilePictureSize)
@ -619,8 +628,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel
super.prepareForReuse()
unloadContent?()
let viewsToMove = [ bubbleView, profilePictureView, replyButton, timerView, messageStatusImageView ]
viewsToMove.forEach { $0.transform = .identity }
viewsToMoveForReply.forEach { $0.transform = .identity }
replyButton.alpha = 0
timerView.prepareForReuse()
}
@ -726,9 +734,6 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let cellViewModel: MessageViewModel = self.viewModel else { return }
let viewsToMove: [UIView] = [
bubbleView, bubbleBackgroundView, profilePictureView, replyButton, timerView, messageStatusImageView
]
let translationX = gestureRecognizer.translation(in: self).x.clamp(-CGFloat.greatestFiniteMagnitude, 0)
switch gestureRecognizer.state {
@ -739,7 +744,7 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel
let damping: CGFloat = 20
let sign: CGFloat = -1
let x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
viewsToMove.forEach { $0.transform = CGAffineTransform(translationX: x, y: 0) }
viewsToMoveForReply.forEach { $0.transform = CGAffineTransform(translationX: x, y: 0) }
if timerView.isHidden {
replyButton.alpha = abs(translationX) / VisibleMessageCell.maxBubbleTranslationX
} else {
@ -778,10 +783,9 @@ final class VisibleMessageCell: MessageCell, UITextViewDelegate, BodyTextViewDel
}
private func resetReply() {
let viewsToMove = [ bubbleView, profilePictureView, replyButton, timerView, messageStatusImageView ]
UIView.animate(withDuration: 0.25) {
viewsToMove.forEach { $0.transform = .identity }
self.replyButton.alpha = 0
UIView.animate(withDuration: 0.25) { [weak self] in
self?.viewsToMoveForReply.forEach { $0.transform = .identity }
self?.replyButton.alpha = 0
}
}

View File

@ -53,8 +53,6 @@ CGFloat kIconViewLength = 24;
return self;
}
[self commonInit];
return self;
}
@ -65,8 +63,6 @@ CGFloat kIconViewLength = 24;
return self;
}
[self commonInit];
return self;
}
@ -77,32 +73,11 @@ CGFloat kIconViewLength = 24;
return self;
}
[self commonInit];
return self;
}
- (void)commonInit
{
[self observeNotifications];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark
- (void)observeNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(otherUsersProfileDidChange:)
name:NSNotification.otherUsersProfileDidChange
object:nil];
}
- (void)configureWithThreadId:(NSString *)threadId threadName:(NSString *)threadName isClosedGroup:(BOOL)isClosedGroup isOpenGroup:(BOOL)isOpenGroup isNoteToSelf:(BOOL)isNoteToSelf {
self.threadId = threadId;
self.threadName = threadName;
@ -964,9 +939,10 @@ CGFloat kIconViewLength = 24;
#pragma mark - Notifications
// FIXME: When this screen gets refactored, make sure to observe changes for relevant profile image updates
- (void)otherUsersProfileDidChange:(NSNotification *)notification
{
NSString *recipientId = notification.userInfo[NSNotification.profileRecipientIdKey];
NSString *recipientId = @"";//notification.userInfo[NSNotification.profileRecipientIdKey];
OWSAssertDebug(recipientId.length > 0);
if (recipientId.length > 0 && !self.isClosedGroup && !self.isOpenGroup && self.threadId == recipientId) {

View File

@ -518,6 +518,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
show(
threadViewModel.threadId,
variant: threadViewModel.threadVariant,
isMessageRequest: (threadViewModel.threadIsMessageRequest == true),
with: .none,
focusedInteractionId: nil,
animated: true
@ -651,6 +652,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
func show(
_ threadId: String,
variant: SessionThread.Variant,
isMessageRequest: Bool,
with action: ConversationViewModel.Action,
focusedInteractionId: Int64?,
animated: Bool
@ -659,8 +661,17 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
presentedVC.dismiss(animated: false, completion: nil)
}
let conversationVC: ConversationVC = ConversationVC(threadId: threadId, threadVariant: variant, focusedInteractionId: focusedInteractionId)
self.navigationController?.setViewControllers([ self, conversationVC ], animated: animated)
let finalViewControllers: [UIViewController] = [
self,
(isMessageRequest ? MessageRequestsViewController() : nil),
ConversationVC(
threadId: threadId,
threadVariant: variant,
focusedInteractionId: focusedInteractionId
)
].compactMap { $0 }
self.navigationController?.setViewControllers(finalViewControllers, animated: animated)
}
@objc private func openSettings() {

View File

@ -142,14 +142,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
AppReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in
self?.handleActivation()
}
/// Clear all notifications whenever we become active
///
/// **Note:** It looks like when opening the app from a notification, `userNotificationCenter(didReceive)` is
/// no longer always called before we become active so we need to dispatch this to run on the next run loop
DispatchQueue.main.async { [weak self] in
self?.clearAllNotificationsAndRestoreBadgeCount()
/// Clear all notifications whenever we become active once the app is ready
///
/// **Note:** It looks like when opening the app from a notification, `userNotificationCenter(didReceive)` is
/// no longer always called before `applicationDidBecomeActive` we need to trigger the "clear notifications" logic
/// within the `runNowOrWhenAppDidBecomeReady` callback and dispatch to the next run loop to ensure it runs after
/// the notification has actually been handled
DispatchQueue.main.async { [weak self] in
self?.clearAllNotificationsAndRestoreBadgeCount()
}
}
// On every activation, clear old temp directories.

View File

@ -10,15 +10,21 @@ public struct SessionApp {
// MARK: - View Convenience Methods
public static func presentConversation(for threadId: String, action: ConversationViewModel.Action = .none, animated: Bool) {
let maybeThread: SessionThread? = Storage.shared.write { db in
try SessionThread.fetchOrCreate(db, id: threadId, variant: .contact)
let maybeThreadInfo: (thread: SessionThread, isMessageRequest: Bool)? = Storage.shared.write { db in
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: threadId, variant: .contact)
return (thread, thread.isMessageRequest(db))
}
guard let variant: SessionThread.Variant = maybeThread?.variant else { return }
guard
let variant: SessionThread.Variant = maybeThreadInfo?.thread.variant,
let isMessageRequest: Bool = maybeThreadInfo?.isMessageRequest
else { return }
self.presentConversation(
for: threadId,
threadVariant: variant,
isMessageRequest: isMessageRequest,
action: action,
focusInteractionId: nil,
animated: animated
@ -28,6 +34,7 @@ public struct SessionApp {
public static func presentConversation(
for threadId: String,
threadVariant: SessionThread.Variant,
isMessageRequest: Bool,
action: ConversationViewModel.Action,
focusInteractionId: Int64?,
animated: Bool
@ -37,6 +44,7 @@ public struct SessionApp {
self.presentConversation(
for: threadId,
threadVariant: threadVariant,
isMessageRequest: isMessageRequest,
action: action,
focusInteractionId: focusInteractionId,
animated: animated
@ -48,6 +56,7 @@ public struct SessionApp {
homeViewController.wrappedValue?.show(
threadId,
variant: threadVariant,
isMessageRequest: isMessageRequest,
with: action,
focusedInteractionId: focusInteractionId,
animated: animated

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,5 +659,5 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -659,4 +659,4 @@
"CHATS_TITLE" = "Chats";
"MESSAGE_TRIMMING_TITLE" = "Message Trimming";
"MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app";
"MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete messages which are older than 6 months from open groups with over 2,000 messages when starting the app";

View File

@ -142,31 +142,11 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
}
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) {
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
// If the thread is a message request and the user hasn't hidden message requests then we need
// to check if this is the only message request thread (group threads can't be message requests
// so just ignore those and if the user has hidden message requests then we want to show the
// notification regardless of how many message requests there are)
if thread.variant == .contact {
if isMessageRequest && !db[.hasHiddenMessageRequests] {
let numMessageRequestThreads: Int = (try? SessionThread
.messageRequestsQuery(userPublicKey: userPublicKey, includeNonVisible: true)
.fetchCount(db))
.defaulting(to: 0)
// Allow this to show a notification if there are no message requests (ie. this is the first one)
guard numMessageRequestThreads == 0 else { return }
}
else if isMessageRequest && db[.hasHiddenMessageRequests] {
// If there are other interactions on this thread already then don't show the notification
if ((try? thread.interactions.fetchCount(db)) ?? 0) > 1 { return }
db[.hasHiddenMessageRequests] = false
}
// Ensure we should be showing a notification for the thread
guard thread.shouldShowNotification(db, for: interaction, isMessageRequest: isMessageRequest) else {
return
}
let identifier: String = interaction.notificationIdentifier(isBackgroundPoll: isBackgroundPoll)
@ -180,11 +160,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
// see https://developer.apple.com/documentation/uikit/uilocalnotification/1616646-alertbody
// for more details.
let messageText: String? = String.filterNotificationText(rawMessageText)
// Don't fire the notification if the current user isn't mentioned
// and isOnlyNotifyingForMentions is on.
guard !thread.onlyNotifyForMentions || interaction.hasMention else { return }
let notificationTitle: String?
var notificationBody: String?

View File

@ -170,7 +170,8 @@ final class DisplayNameVC: BaseVC {
ProfileManager.updateLocal(
queue: DispatchQueue.global(qos: .default),
profileName: displayName,
avatarImage: nil,
image: nil,
imageFilePath: nil,
requiredSync: false
)
let pnModeVC = PNModeVC()

View File

@ -14,9 +14,6 @@ class ChatSettingsViewController: OWSTableViewController {
self.updateTableContents()
ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: "CHATS_TITLE".localized(), hasCustomBackButton: false)
let closeButton: UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "X"), style: .plain, target: self, action: #selector(close(_:)))
self.navigationItem.leftBarButtonItem = closeButton
}
override func viewDidAppear(_ animated: Bool) {
@ -47,9 +44,11 @@ class ChatSettingsViewController: OWSTableViewController {
// MARK: - Actions
@objc private func didToggleTrimOpenGroupsSwitch(_ sender: UISwitch) {
let switchIsOn: Bool = sender.isOn
Storage.shared.writeAsync(
updates: { db in
db[.trimOpenGroupMessagesOlderThanSixMonths] = !sender.isOn
db[.trimOpenGroupMessagesOlderThanSixMonths] = !switchIsOn
},
completion: { [weak self] _, _ in
self?.updateTableContents()

View File

@ -7,7 +7,6 @@ import SessionMessagingKit
import SignalUtilitiesKit
final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
private var profilePictureToBeUploaded: UIImage?
private var displayNameToBeUploaded: String?
private var isEditingDisplayName = false { didSet { handleIsEditingDisplayNameChanged() } }
@ -419,34 +418,47 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
}
}
func avatarDidChange(_ image: UIImage) {
let maxSize = Int(ProfileManager.maxAvatarDiameter)
profilePictureToBeUploaded = image.resizedImage(toFillPixelSize: CGSize(width: maxSize, height: maxSize))
updateProfile(isUpdatingDisplayName: false, isUpdatingProfilePicture: true)
func avatarDidChange(_ image: UIImage?, filePath: String?) {
updateProfile(
profilePicture: image,
profilePictureFilePath: filePath,
isUpdatingDisplayName: false,
isUpdatingProfilePicture: true
)
}
func clearAvatar() {
profilePictureToBeUploaded = nil
updateProfile(isUpdatingDisplayName: false, isUpdatingProfilePicture: true)
updateProfile(
profilePicture: nil,
profilePictureFilePath: nil,
isUpdatingDisplayName: false,
isUpdatingProfilePicture: true
)
}
private func updateProfile(isUpdatingDisplayName: Bool, isUpdatingProfilePicture: Bool) {
private func updateProfile(
profilePicture: UIImage?,
profilePictureFilePath: String?,
isUpdatingDisplayName: Bool,
isUpdatingProfilePicture: Bool
) {
let userDefaults = UserDefaults.standard
let name: String? = (displayNameToBeUploaded ?? Profile.fetchOrCreateCurrentUser().name)
let profilePicture: UIImage? = (profilePictureToBeUploaded ?? ProfileManager.profileAvatar(id: getUserHexEncodedPublicKey()))
let imageFilePath: String? = (profilePictureFilePath ?? ProfileManager.profileAvatarFilepath(id: getUserHexEncodedPublicKey()))
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self, displayNameToBeUploaded, profilePictureToBeUploaded] modalActivityIndicator in
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self, displayNameToBeUploaded] modalActivityIndicator in
ProfileManager.updateLocal(
queue: DispatchQueue.global(qos: .default),
profileName: (name ?? ""),
avatarImage: profilePicture,
image: profilePicture,
imageFilePath: imageFilePath,
requiredSync: true,
success: { db, updatedProfile in
if displayNameToBeUploaded != nil {
userDefaults[.lastDisplayNameUpdate] = Date()
}
if profilePictureToBeUploaded != nil {
if isUpdatingProfilePicture {
userDefaults[.lastProfilePictureUpdate] = Date()
}
@ -462,7 +474,6 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
threadVariant: .contact
)
self?.displayNameLabel.text = name
self?.profilePictureToBeUploaded = nil
self?.displayNameToBeUploaded = nil
}
}
@ -556,7 +567,12 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
}
isEditingDisplayName = false
displayNameToBeUploaded = displayName
updateProfile(isUpdatingDisplayName: true, isUpdatingProfilePicture: false)
updateProfile(
profilePicture: nil,
profilePictureFilePath: nil,
isUpdatingDisplayName: true,
isUpdatingProfilePicture: false
)
}
@objc private func showEditProfilePictureUI() {

View File

@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable NSString *)avatarActionSheetTitle;
- (void)avatarDidChange:(UIImage *)image;
- (void)avatarDidChange:(nullable UIImage *)image filePath:(nullable NSString *)filePath;
- (UIViewController *)fromViewController;

View File

@ -123,19 +123,34 @@ NS_ASSUME_NONNULL_BEGIN
[SNAppearance switchToSessionAppearance];
NSURL* imageURL = [info objectForKey:UIImagePickerControllerImageURL];
UIImage *rawAvatar = [info objectForKey:UIImagePickerControllerOriginalImage];
[self.delegate.fromViewController
dismissViewControllerAnimated:YES
completion:^{
OWSAssertIsOnMainThread();
// Check if the user selected an animated image (if so then don't crop, just
// set the avatar directly
NSString *type;
if ([imageURL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:nil]) {
if ([[MIMETypeUtil supportedAnimatedImageUTITypes] containsObject:type]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate avatarDidChange:nil filePath: imageURL.path];
});
return;
}
}
if (rawAvatar) {
OWSAssertIsOnMainThread();
CropScaleImageViewController *vc = [[CropScaleImageViewController alloc]
initWithSrcImage:rawAvatar
successCompletion:^(UIImage *_Nonnull dstImage) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate avatarDidChange:dstImage];
[self.delegate avatarDidChange:dstImage filePath:nil];
});
}];
[self.delegate.fromViewController presentViewController:vc

View File

@ -17,8 +17,6 @@ import UIKit
internal func findFrontmostViewController(ignoringAlerts: Bool) -> UIViewController? {
guard let window: UIWindow = CurrentAppContext().mainWindow else { return nil }
Logger.error("findFrontmostViewController: \(window)")
guard let viewController: UIViewController = window.rootViewController else {
owsFailDebug("Missing root view controller.")
return nil

View File

@ -1465,6 +1465,9 @@ enum _003_YDBToGRDBMigration: Migration {
db[.hasSentAMessage] = (legacyPreferences[SMKLegacy.preferencesKeyHasSentAMessageKey] as? Bool == true)
db[.isReadyForAppExtensions] = CurrentAppContext().appUserDefaults().bool(forKey: SMKLegacy.preferencesKeyIsReadyForAppExtensions)
// We want this setting to be on by default
db[.trimOpenGroupMessagesOlderThanSixMonths] = true
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
}

View File

@ -1049,7 +1049,10 @@ extension Attachment {
uploadPromise
.done(on: queue) { fileId in
// Save the final upload info
/// Save the final upload info
///
/// **Note:** We **MUST** use the `.with` function here to ensure the `isValid` flag is
/// updated correctly
let uploadedAttachment: Attachment? = Storage.shared.write { db in
try updatedAttachment?
.with(

View File

@ -72,31 +72,6 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco
)
"""
}
// MARK: - PersistableRecord
public func save(_ db: Database) throws {
let oldProfile: Profile? = try? Profile.fetchOne(db, id: id)
try performSave(db)
db.afterNextTransactionCommit { db in
// Delete old profile picture if needed
if let oldProfilePictureFileName: String = oldProfile?.profilePictureFileName, oldProfilePictureFileName != profilePictureFileName {
let path: String = ProfileManager.profileAvatarFilepath(filename: oldProfilePictureFileName)
DispatchQueue.global(qos: .default).async {
OWSFileSystem.deleteFileIfExists(path)
}
}
// FIXME: Remove this once the OWSConversationSettingsViewController has been refactored and is observing DB changes
if id != getUserHexEncodedPublicKey(db) {
let userInfo = [ Notification.Key.profileRecipientId.rawValue: id ]
NotificationCenter.default.post(name: .otherUsersProfileDidChange, object: nil, userInfo: userInfo)
}
}
}
}
// MARK: - Codable

View File

@ -174,7 +174,12 @@ public extension SessionThread {
(includeNonVisible || shouldBeVisible) &&
variant == .contact &&
id != getUserHexEncodedPublicKey(db) && // Note to self
(try? Contact.fetchOne(db, id: id))?.isApproved != true
(try? Contact
.filter(id: id)
.select(.isApproved)
.asRequest(of: Bool.self)
.fetchOne(db))
.defaulting(to: false) == false
)
}
}
@ -196,7 +201,7 @@ public extension SessionThread {
"""
}
static func unreadMessageRequestsThreadIdQuery(userPublicKey: String) -> SQLRequest<Int64> {
static func unreadMessageRequestsThreadIdQuery(userPublicKey: String, includeNonVisible: Bool = false) -> SQLRequest<String> {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()
@ -210,7 +215,7 @@ public extension SessionThread {
)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
WHERE (
\(SessionThread.isMessageRequest(userPublicKey: userPublicKey))
\(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: includeNonVisible))
)
GROUP BY \(thread[.id])
"""
@ -245,6 +250,50 @@ public extension SessionThread {
)
}
func shouldShowNotification(_ db: Database, for interaction: Interaction, isMessageRequest: Bool) -> Bool {
// Ensure that the thread isn't muted and either the thread isn't only notifying for mentions
// or the user was actually mentioned
guard
Date().timeIntervalSince1970 > (self.mutedUntilTimestamp ?? 0) &&
(
self.variant == .contact ||
!self.onlyNotifyForMentions ||
interaction.hasMention
)
else { return false }
let userPublicKey: String = getUserHexEncodedPublicKey(db)
// No need to notify the user for self-send messages
guard interaction.authorId != userPublicKey else { return false }
// If the thread is a message request then we only want to notify for the first message
if self.variant == .contact && isMessageRequest {
let hasHiddenMessageRequests: Bool = db[.hasHiddenMessageRequests]
// If the user hasn't hidden the message requests section then only show the notification if
// all the other message request threads have been read
if !hasHiddenMessageRequests {
let numUnreadMessageRequestThreads: Int = (try? SessionThread
.unreadMessageRequestsThreadIdQuery(userPublicKey: userPublicKey, includeNonVisible: true)
.fetchCount(db))
.defaulting(to: 1)
guard numUnreadMessageRequestThreads == 1 else { return false }
}
// We only want to show a notification for the first interaction in the thread
guard ((try? self.interactions.fetchCount(db)) ?? 0) <= 1 else { return false }
// Need to re-show the message requests section if it had been hidden
if hasHiddenMessageRequests {
db[.hasHiddenMessageRequests] = false
}
}
return true
}
static func displayName(
threadId: String,
variant: Variant,

View File

@ -1,21 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import SessionUtilitiesKit
// FIXME: Remove these extensions once the OWSConversationSettingsViewModel is refactored to swift and uses proper database observation
public extension Notification.Name {
static let otherUsersProfileDidChange = Notification.Name("otherUsersProfileDidChange")
}
@objc public extension NSNotification {
@objc static let otherUsersProfileDidChange = Notification.Name.otherUsersProfileDidChange.rawValue as NSString
}
extension Notification.Key {
static let profileRecipientId = Notification.Key("profileRecipientId")
}
@objc public extension NSNotification {
static let profileRecipientIdKey = Notification.Key.profileRecipientId.rawValue as NSString
}

View File

@ -16,6 +16,7 @@ public enum GarbageCollectionJob: JobExecutor {
public static var requiresThreadId: Bool = false
public static let requiresInteractionId: Bool = false
public static let approxSixMonthsInSeconds: TimeInterval = (6 * 30 * 24 * 60 * 60)
private static let minInteractionsToTrim: Int = 2000
public static func run(
_ job: Job,
@ -68,6 +69,8 @@ public enum GarbageCollectionJob: JobExecutor {
if typesToCollect.contains(.oldOpenGroupMessages) && db[.trimOpenGroupMessagesOlderThanSixMonths] {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let threadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name)
let minInteractionsToTrimSql: SQL = SQL("\(GarbageCollectionJob.minInteractionsToTrim)")
try db.execute(literal: """
DELETE FROM \(Interaction.self)
@ -78,7 +81,17 @@ public enum GarbageCollectionJob: JobExecutor {
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
\(thread[.id]) = \(interaction[.threadId])
)
WHERE \(interaction[.timestampMs]) < \(timestampNow - approxSixMonthsInSeconds)
JOIN (
SELECT
COUNT(\(interaction.alias[Column.rowID])) AS interactionCount,
\(interaction[.threadId])
FROM \(Interaction.self)
GROUP BY \(interaction[.threadId])
) AS interactionInfo ON interactionInfo.\(threadIdLiteral) = \(interaction[.threadId])
WHERE (
\(interaction[.timestampMs]) < \(timestampNow - approxSixMonthsInSeconds) AND
interactionInfo.interactionCount >= \(minInteractionsToTrimSql)
)
)
""")
}

View File

@ -17,10 +17,7 @@ public enum NotifyPushServerJob: JobExecutor {
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
) {
let server: String = PushNotificationAPI.server
guard
let url: URL = URL(string: "\(server)/notify"),
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
@ -28,34 +25,16 @@ public enum NotifyPushServerJob: JobExecutor {
return
}
let requestBody: RequestBody = RequestBody(
data: details.message.data.description,
sendTo: details.message.recipient
)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
failure(job, HTTP.Error.invalidJSON, true)
return
}
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ]
request.httpBody = body
attempt(maxRetryCount: 4, recoveringOn: queue) {
OnionRequestAPI
.sendOnionRequest(
request,
to: server,
using: .v2,
with: PushNotificationAPI.serverPublicKey
)
.map { _ in }
}
.done(on: queue) { _ in success(job, false) }
.catch(on: queue) { error in failure(job, error, false) }
.retainUntilComplete()
PushNotificationAPI
.notify(
recipient: details.message.recipient,
with: details.message.data,
maxRetryCount: 4,
queue: queue
)
.done(on: queue) { _ in success(job, false) }
.catch(on: queue) { error in failure(job, error, false) }
.retainUntilComplete()
}
}
@ -65,14 +44,4 @@ extension NotifyPushServerJob {
public struct Details: Codable {
public let message: SnodeMessage
}
struct RequestBody: Codable {
enum CodingKeys: String, CodingKey {
case data
case sendTo = "send_to"
}
let data: String
let sendTo: String
}
}

View File

@ -34,12 +34,14 @@ public enum UpdateProfilePictureJob: JobExecutor {
// Note: The user defaults flag is updated in ProfileManager
let profile: Profile = Profile.fetchOrCreateCurrentUser()
let profilePicture: UIImage? = ProfileManager.profileAvatar(id: profile.id)
let profileFilePath: String? = profile.profilePictureFileName
.map { ProfileManager.profileAvatarFilepath(filename: $0) }
ProfileManager.updateLocal(
queue: queue,
profileName: profile.name,
avatarImage: profilePicture,
image: nil,
imageFilePath: profileFilePath,
requiredSync: true,
success: { _, _ in success(job, false) },
failure: { error in failure(job, error, false) }

View File

@ -224,6 +224,15 @@ public enum MessageReceiver {
default: fatalError()
}
// Perform any required post-handling logic
try MessageReceiver.postHandleMessage(db, message: message, openGroupId: openGroupId)
}
public static func postHandleMessage(
_ db: Database,
message: Message,
openGroupId: String?
) throws {
// When handling any non-typing indicator message we want to make sure the thread becomes
// visible (the only other spot this flag gets set is when sending messages)
switch message {

View File

@ -7,11 +7,21 @@ import SessionUtilitiesKit
@objc(LKPushNotificationAPI)
public final class PushNotificationAPI : NSObject {
struct RequestBody: Codable {
struct RegistrationRequestBody: Codable {
let token: String
let pubKey: String?
}
struct NotifyRequestBody: Codable {
enum CodingKeys: String, CodingKey {
case data
case sendTo = "send_to"
}
let data: String
let sendTo: String
}
struct ClosedGroupRequestBody: Codable {
let closedGroupPublicKey: String
let pubKey: String
@ -42,7 +52,7 @@ public final class PushNotificationAPI : NSObject {
// MARK: - Registration
public static func unregister(_ token: Data) -> Promise<Void> {
let requestBody: RequestBody = RequestBody(token: token.toHexString(), pubKey: nil)
let requestBody: RegistrationRequestBody = RegistrationRequestBody(token: token.toHexString(), pubKey: nil)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
return Promise(error: HTTP.Error.invalidJSON)
@ -92,7 +102,7 @@ public final class PushNotificationAPI : NSObject {
public static func register(with token: Data, publicKey: String, isForcedUpdate: Bool) -> Promise<Void> {
let hexEncodedToken: String = token.toHexString()
let requestBody: RequestBody = RequestBody(token: hexEncodedToken, pubKey: publicKey)
let requestBody: RegistrationRequestBody = RegistrationRequestBody(token: hexEncodedToken, pubKey: publicKey)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
return Promise(error: HTTP.Error.invalidJSON)
@ -203,4 +213,33 @@ public final class PushNotificationAPI : NSObject {
public static func objc_performOperation(_ operation: ClosedGroupOperation, for closedGroupPublicKey: String, publicKey: String) -> AnyPromise {
return AnyPromise.from(performOperation(operation, for: closedGroupPublicKey, publicKey: publicKey))
}
// MARK: - Notify
public static func notify(
recipient: String,
with message: String,
maxRetryCount: UInt? = nil,
queue: DispatchQueue = DispatchQueue.global()
) -> Promise<Void> {
let requestBody: NotifyRequestBody = NotifyRequestBody(data: message, sendTo: recipient)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
return Promise(error: HTTP.Error.invalidJSON)
}
let url = URL(string: "\(server)/notify")!
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ]
request.httpBody = body
let retryCount: UInt = (maxRetryCount ?? PushNotificationAPI.maxRetryCount)
let promise: Promise<Void> = attempt(maxRetryCount: retryCount, recoveringOn: queue) {
OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey)
.map { _ in }
}
return promise
}
}

View File

@ -366,9 +366,10 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
// Only incoming messages
(self.variant == .standardIncoming || self.variant == .standardIncomingDeleted) &&
// Show if the next message has a different sender or has a "date break"
// Show if the next message has a different sender, isn't a standard message or has a "date break"
(
self.authorId != nextModel?.authorId ||
(nextModel?.variant != .standardIncoming && nextModel?.variant != .standardIncomingDeleted) ||
shouldShowDateOnNextModel
) &&

View File

@ -574,7 +574,6 @@ public extension SessionThreadViewModel {
(\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey),
(
\(thread[.shouldBeVisible]) = true AND
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND
\(SQL("\(thread[.id]) != \(userPublicKey)")) AND
IFNULL(\(contact[.isApproved]), false) = false

View File

@ -12,7 +12,7 @@ public struct ProfileManager {
private static let nameDataLength: UInt = 26
public static let maxAvatarDiameter: CGFloat = 640
private static var profileAvatarCache: Atomic<[String: UIImage]> = Atomic([:])
private static var profileAvatarCache: Atomic<[String: Data]> = Atomic([:])
private static var currentAvatarDownloads: Atomic<Set<String>> = Atomic([])
// MARK: - Functions
@ -21,7 +21,7 @@ public struct ProfileManager {
return ((profileName.data(using: .utf8)?.count ?? 0) > nameDataLength)
}
public static func profileAvatar(_ db: Database? = nil, id: String) -> UIImage? {
public static func profileAvatar(_ db: Database? = nil, id: String) -> Data? {
guard let db: Database = db else {
return Storage.shared.read { db in profileAvatar(db, id: id) }
}
@ -30,9 +30,9 @@ public struct ProfileManager {
return profileAvatar(profile: profile)
}
public static func profileAvatar(profile: Profile) -> UIImage? {
public static func profileAvatar(profile: Profile) -> Data? {
if let profileFileName: String = profile.profilePictureFileName, !profileFileName.isEmpty {
return loadProfileAvatar(for: profileFileName)
return loadProfileAvatar(for: profileFileName, profile: profile)
}
if let profilePictureUrl: String = profile.profilePictureUrl, !profilePictureUrl.isEmpty {
@ -42,22 +42,36 @@ public struct ProfileManager {
return nil
}
private static func loadProfileAvatar(for fileName: String) -> UIImage? {
if let cachedImage: UIImage = profileAvatarCache.wrappedValue[fileName] {
return cachedImage
private static func loadProfileAvatar(for fileName: String, profile: Profile) -> Data? {
if let cachedImageData: Data = profileAvatarCache.wrappedValue[fileName] {
return cachedImageData
}
guard
!fileName.isEmpty,
let data: Data = loadProfileData(with: fileName),
data.isValidImage,
let image: UIImage = UIImage(data: data)
data.isValidImage
else {
// If we can't load the avatar or it's an invalid/corrupted image then clear out
// the 'profilePictureFileName' and try to re-download
Storage.shared.writeAsync(
updates: { db in
_ = try? Profile
.filter(id: profile.id)
.updateAll(db, Profile.Columns.profilePictureFileName.set(to: nil))
},
completion: { _, _ in
// Try to re-download the avatar if it has a URL
if let profilePictureUrl: String = profile.profilePictureUrl, !profilePictureUrl.isEmpty {
downloadAvatar(for: profile)
}
}
)
return nil
}
profileAvatarCache.mutate { $0[fileName] = image }
return image
profileAvatarCache.mutate { $0[fileName] = data }
return data
}
private static func loadProfileData(with fileName: String) -> Data? {
@ -98,6 +112,20 @@ public struct ProfileManager {
return path
}()
public static func profileAvatarFilepath(_ db: Database? = nil, id: String) -> String? {
guard let db: Database = db else {
return Storage.shared.read { db in profileAvatarFilepath(db, id: id) }
}
let maybeFileName: String? = try? Profile
.filter(id: id)
.select(.profilePictureFileName)
.asRequest(of: String.self)
.fetchOne(db)
return maybeFileName.map { ProfileManager.profileAvatarFilepath(filename: $0) }
}
public static func profileAvatarFilepath(filename: String) -> String {
guard !filename.isEmpty else { return "" }
@ -148,45 +176,46 @@ public struct ProfileManager {
.done(on: queue) { data in
currentAvatarDownloads.mutate { $0.remove(profile.id) }
guard let latestProfile: Profile = Storage.shared.read({ db in try Profile.fetchOne(db, id: profile.id) }) else {
return
}
guard
let latestProfileKey: OWSAES256Key = latestProfile.profileEncryptionKey,
!latestProfileKey.keyData.isEmpty,
latestProfileKey == profileKeyAtStart
else {
OWSLogger.warn("Ignoring avatar download for obsolete user profile.")
return
}
guard profileUrlStringAtStart == latestProfile.profilePictureUrl else {
OWSLogger.warn("Avatar url has changed during download.")
if latestProfile.profilePictureUrl?.isEmpty == false {
self.downloadAvatar(for: latestProfile)
}
return
}
guard let decryptedData: Data = decryptProfileData(data: data, key: profileKeyAtStart) else {
OWSLogger.warn("Avatar data for \(profile.id) could not be decrypted.")
return
}
try? decryptedData.write(to: URL(fileURLWithPath: filePath), options: [.atomic])
guard UIImage(contentsOfFile: filePath) != nil else {
OWSLogger.warn("Avatar image for \(profile.id) could not be loaded.")
return
}
// Store the updated 'profilePictureFileName'
Storage.shared.write { db in
guard let latestProfile: Profile = try Profile.fetchOne(db, id: profile.id) else {
return
}
guard
let latestProfileKey: OWSAES256Key = latestProfile.profileEncryptionKey,
!latestProfileKey.keyData.isEmpty,
latestProfileKey == profileKeyAtStart
else {
OWSLogger.warn("Ignoring avatar download for obsolete user profile.")
return
}
guard profileUrlStringAtStart == latestProfile.profilePictureUrl else {
OWSLogger.warn("Avatar url has changed during download.")
if latestProfile.profilePictureUrl?.isEmpty == false {
self.downloadAvatar(for: latestProfile)
}
return
}
guard let decryptedData: Data = decryptProfileData(data: data, key: profileKeyAtStart) else {
OWSLogger.warn("Avatar data for \(profile.id) could not be decrypted.")
return
}
try? decryptedData.write(to: URL(fileURLWithPath: filePath), options: [.atomic])
guard let image: UIImage = UIImage(contentsOfFile: filePath) else {
OWSLogger.warn("Avatar image for \(profile.id) could not be loaded.")
return
}
_ = try? Profile
.filter(id: profile.id)
.updateAll(db, Profile.Columns.profilePictureFileName.set(to: fileName))
profileAvatarCache.mutate { $0[fileName] = image }
profileAvatarCache.mutate { $0[fileName] = decryptedData }
}
// Redundant but without reading 'backgroundTask' it will warn that the variable
@ -209,7 +238,8 @@ public struct ProfileManager {
public static func updateLocal(
queue: DispatchQueue,
profileName: String,
avatarImage: UIImage?,
image: UIImage?,
imageFilePath: String?,
requiredSync: Bool,
success: ((Database, Profile) throws -> ())? = nil,
failure: ((ProfileManagerError) -> ())? = nil
@ -218,8 +248,60 @@ public struct ProfileManager {
// If the profile avatar was updated or removed then encrypt with a new profile key
// to ensure that other users know that our profile picture was updated
let newProfileKey: OWSAES256Key = OWSAES256Key.generateRandom()
let maxAvatarBytes: UInt = (5 * 1000 * 1000)
let avatarImageData: Data?
guard let avatarImage: UIImage = avatarImage else {
do {
avatarImageData = try {
guard var image: UIImage = image else {
guard let imageFilePath: String = imageFilePath else { return nil }
let data: Data = try Data(contentsOf: URL(fileURLWithPath: imageFilePath))
guard data.count <= maxAvatarBytes else {
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't
// be able to fit our profile photo (eg. generating pure noise at our resolution
// compresses to ~200k)
SNLog("Animated profile avatar was too large.")
SNLog("Updating service with profile failed.")
throw ProfileManagerError.avatarUploadMaxFileSizeExceeded
}
return data
}
if image.size.width != maxAvatarDiameter || image.size.height != maxAvatarDiameter {
// To help ensure the user is being shown the same cropping of their avatar as
// everyone else will see, we want to be sure that the image was resized before this point.
SNLog("Avatar image should have been resized before trying to upload")
image = image.resizedImage(toFillPixelSize: CGSize(width: maxAvatarDiameter, height: maxAvatarDiameter))
}
guard let data: Data = image.jpegData(compressionQuality: 0.95) else {
SNLog("Updating service with profile failed.")
throw ProfileManagerError.avatarWriteFailed
}
guard data.count <= maxAvatarBytes else {
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't
// be able to fit our profile photo (eg. generating pure noise at our resolution
// compresses to ~200k)
SNLog("Suprised to find profile avatar was too large. Was it scaled properly? image: \(image)")
SNLog("Updating service with profile failed.")
throw ProfileManagerError.avatarUploadMaxFileSizeExceeded
}
return data
}()
}
catch {
if let profileManagerError: ProfileManagerError = error as? ProfileManagerError {
failure?(profileManagerError)
}
return
}
guard let data: Data = avatarImageData else {
// If we have no image then we need to make sure to remove it from the profile
Storage.shared.writeAsync { db in
let existingProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
@ -255,39 +337,18 @@ public struct ProfileManager {
// If we have a new avatar image, we must first:
//
// * Encode it to JPEG.
// * Write it to disk.
// * Encrypt it
// * Upload it to asset service
// * Send asset service info to Signal Service
OWSLogger.verbose("Updating local profile on service with new avatar.")
let maxAvatarBytes: UInt = (5 * 1000 * 1000)
var image: UIImage = avatarImage
if image.size.width != maxAvatarDiameter || image.size.height != maxAvatarDiameter {
// To help ensure the user is being shown the same cropping of their avatar as
// everyone else will see, we want to be sure that the image was resized before this point.
SNLog("Avatar image should have been resized before trying to upload")
image = image.resizedImage(toFillPixelSize: CGSize(width: maxAvatarDiameter, height: maxAvatarDiameter))
}
guard let data: Data = image.jpegData(compressionQuality: 0.95) else {
SNLog("Updating service with profile failed.")
failure?(.avatarWriteFailed)
return
}
guard data.count <= maxAvatarBytes else {
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't
// be able to fit our profile photo (eg. generating pure noise at our resolution
// compresses to ~200k)
SNLog("Suprised to find profile avatar was too large. Was it scaled properly? image: \(image)")
SNLog("Updating service with profile failed.")
failure?(.avatarImageTooLarge)
return
}
let fileName: String = UUID().uuidString.appendingFileExtension("jpg")
let fileName: String = UUID().uuidString
.appendingFileExtension(
imageFilePath
.map { URL(fileURLWithPath: $0).pathExtension }
.defaulting(to: "jpg")
)
let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName)
// Write the avatar to disk
@ -324,7 +385,7 @@ public struct ProfileManager {
.saved(db)
// Update the cached avatar image value
profileAvatarCache.mutate { $0[fileName] = avatarImage }
profileAvatarCache.mutate { $0[fileName] = data }
SNLog("Successfully updated service with profile.")
try success?(db, profile)

View File

@ -10,43 +10,14 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
private var notifications: [String: UNNotificationRequest] = [:]
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) {
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
// If the thread is a message request and the user hasn't hidden message requests then we need
// to check if this is the only message request thread (group threads can't be message requests
// so just ignore those and if the user has hidden message requests then we want to show the
// notification regardless of how many message requests there are)
if thread.variant == .contact {
if isMessageRequest && !db[.hasHiddenMessageRequests] {
let numMessageRequestThreads: Int = (try? SessionThread
.messageRequestsQuery(userPublicKey: userPublicKey, includeNonVisible: true)
.fetchCount(db))
.defaulting(to: 0)
// Allow this to show a notification if there are no message requests (ie. this is the first one)
guard numMessageRequestThreads == 0 else { return }
}
else if isMessageRequest && db[.hasHiddenMessageRequests] {
// If there are other interactions on this thread already then don't show the notification
if ((try? thread.interactions.fetchCount(db)) ?? 0) > 1 { return }
db[.hasHiddenMessageRequests] = false
}
}
let senderPublicKey: String = interaction.authorId
guard senderPublicKey != userPublicKey else {
// Ignore PNs for messages sent by the current user
// after handling the message. Otherwise the closed
// group self-send messages won't show.
// Ensure we should be showing a notification for the thread
guard thread.shouldShowNotification(db, for: interaction, isMessageRequest: isMessageRequest) else {
return
}
let senderName: String = Profile.displayName(db, id: senderPublicKey, threadVariant: thread.variant)
let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
var notificationTitle: String = senderName
if thread.variant == .closedGroup || thread.variant == .openGroup {

View File

@ -67,16 +67,26 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
return
}
let maybeVariant: SessionThread.Variant? = processedMessage.threadId
.map { threadId in
try? SessionThread
.filter(id: threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
.fetchOne(db)
}
let isOpenGroup: Bool = (maybeVariant == .openGroup)
switch processedMessage.messageInfo.message {
case let visibleMessage as VisibleMessage:
let interactionId: Int64 = try MessageReceiver.handleVisibleMessage(
db,
message: visibleMessage,
associatedWithProto: processedMessage.proto,
openGroupId: nil,
openGroupId: (isOpenGroup ? processedMessage.threadId : nil),
isBackgroundPoll: false
)
// Remove the notifications if there is an outgoing messages from a linked device
if
let interaction: Interaction = try? Interaction.fetchOne(db, id: interactionId),
@ -127,6 +137,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
default: break
}
// Perform any required post-handling logic
try MessageReceiver.postHandleMessage(
db,
message: processedMessage.messageInfo.message,
openGroupId: (isOpenGroup ? processedMessage.threadId : nil)
)
}
catch {
if let error = error as? MessageReceiverError, error.isRetryable {

View File

@ -16,26 +16,21 @@ internal extension OnionRequestAPI {
}
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
static func encrypt(_ payload: Data, for destination: OnionRequestAPIDestination, with version: OnionRequestAPIVersion) -> Promise<AESGCM.EncryptionResult> {
static func encrypt(_ payload: Data, for destination: OnionRequestAPIDestination) -> Promise<AESGCM.EncryptionResult> {
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
do {
let data: Data
switch version {
case .v2, .v3:
// Wrapping is only needed for snode requests
switch destination {
case .snode: data = try encode(ciphertext: payload, json: [ "headers" : "" ])
case .server: data = payload
}
switch destination {
case .snode(let snode):
// Need to wrap the payload for snode requests
let data: Data = try encode(ciphertext: payload, json: [ "headers" : "" ])
let result: AESGCM.EncryptionResult = try AESGCM.encrypt(data, for: snode.x25519PublicKey)
seal.fulfill(result)
case .v4:
data = payload
case .server(_, _, let serverX25519PublicKey, _, _):
let result: AESGCM.EncryptionResult = try AESGCM.encrypt(payload, for: serverX25519PublicKey)
seal.fulfill(result)
}
let result = try encrypt(data, for: destination)
seal.fulfill(result)
}
catch (let error) {
seal.reject(error)
@ -44,16 +39,6 @@ internal extension OnionRequestAPI {
return promise
}
private static func encrypt(_ payload: Data, for destination: OnionRequestAPIDestination) throws -> AESGCM.EncryptionResult {
switch destination {
case .snode(let snode):
return try AESGCM.encrypt(payload, for: snode.x25519PublicKey)
case .server(_, _, let serverX25519PublicKey, _, _):
return try AESGCM.encrypt(payload, for: serverX25519PublicKey)
}
}
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
static func encryptHop(from lhs: OnionRequestAPIDestination, to rhs: OnionRequestAPIDestination, using previousEncryptionResult: AESGCM.EncryptionResult) -> Promise<AESGCM.EncryptionResult> {

View File

@ -7,7 +7,7 @@ import PromiseKit
import SessionUtilitiesKit
public protocol OnionRequestAPIType {
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, using version: OnionRequestAPIVersion, associatedWith publicKey: String?) -> Promise<Data>
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data>
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)>
}
@ -310,7 +310,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: Data, targetedAt destination: OnionRequestAPIDestination, version: OnionRequestAPIVersion) -> Promise<OnionBuildingResult> {
private static func buildOnion(around payload: Data, targetedAt destination: OnionRequestAPIDestination) -> Promise<OnionBuildingResult> {
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: AESGCM.EncryptionResult!
@ -323,7 +323,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
guardSnode = path.first!
// Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination, with: version)
return encrypt(payload, for: destination)
.then2 { r -> Promise<AESGCM.EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey
@ -356,14 +356,15 @@ public enum OnionRequestAPI: OnionRequestAPIType {
// MARK: - Public API
/// Sends an onion request to `snode`. Builds new paths as needed.
public static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, using version: OnionRequestAPIVersion, associatedWith publicKey: String? = nil) -> Promise<Data> {
public static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<Data> {
let payloadJson: JSON = [ "method" : method.rawValue, "params" : parameters ]
guard let payload: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []) else {
return Promise(error: HTTP.Error.invalidJSON)
}
return sendOnionRequest(with: payload, to: OnionRequestAPIDestination.snode(snode), version: version)
/// **Note:** Currently the service nodes only support V3 Onion Requests
return sendOnionRequest(with: payload, to: OnionRequestAPIDestination.snode(snode), version: .v3)
.map { _, maybeData in
guard let data: Data = maybeData else { throw HTTP.Error.invalidResponse }
@ -410,42 +411,43 @@ public enum OnionRequestAPI: OnionRequestAPIType {
var guardSnode: Snode?
Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
buildOnion(around: payload, targetedAt: destination, version: version).done2 { intermediate in
guardSnode = intermediate.guardSnode
let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2"
let finalEncryptionResult = intermediate.finalEncryptionResult
let onion = finalEncryptionResult.ciphertext
if case OnionRequestAPIDestination.server = destination, Double(onion.count) > 0.75 * Double(maxRequestSize) {
SNLog("Approaching request size limit: ~\(onion.count) bytes.")
}
let parameters: JSON = [
"ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString()
]
let body: Data
do {
body = try encode(ciphertext: onion, json: parameters)
} catch {
return seal.reject(error)
}
let destinationSymmetricKey = intermediate.destinationSymmetricKey
HTTP.execute(.post, url, body: body)
.done2 { responseData in
handleResponse(
responseData: responseData,
destinationSymmetricKey: destinationSymmetricKey,
version: version,
destination: destination,
seal: seal
)
buildOnion(around: payload, targetedAt: destination)
.done2 { intermediate in
guardSnode = intermediate.guardSnode
let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2"
let finalEncryptionResult = intermediate.finalEncryptionResult
let onion = finalEncryptionResult.ciphertext
if case OnionRequestAPIDestination.server = destination, Double(onion.count) > 0.75 * Double(maxRequestSize) {
SNLog("Approaching request size limit: ~\(onion.count) bytes.")
}
.catch2 { error in
seal.reject(error)
let parameters: JSON = [
"ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString()
]
let body: Data
do {
body = try encode(ciphertext: onion, json: parameters)
} catch {
return seal.reject(error)
}
}
.catch2 { error in
seal.reject(error)
}
let destinationSymmetricKey = intermediate.destinationSymmetricKey
HTTP.execute(.post, url, body: body)
.done2 { responseData in
handleResponse(
responseData: responseData,
destinationSymmetricKey: destinationSymmetricKey,
version: version,
destination: destination,
seal: seal
)
}
.catch2 { error in
seal.reject(error)
}
}
.catch2 { error in
seal.reject(error)
}
}
promise.catch2 { error in // Must be invoked on Threading.workQueue

View File

@ -134,7 +134,6 @@ public final class SnodeAPI {
to: snode,
invoking: method,
with: parameters,
using: .v3,
associatedWith: publicKey
)
.map2 { responseData in
@ -207,7 +206,6 @@ public final class SnodeAPI {
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true)
.map2 { responseData -> Set<Snode> in
// TODO: Validate this works
guard let snodePool: SnodePoolResponse = try? JSONDecoder().decode(SnodePoolResponse.self, from: responseData) else {
throw SnodeAPIError.snodePoolUpdatingFailed
}
@ -261,7 +259,6 @@ public final class SnodeAPI {
return invoke(.oxenDaemonRPCCall, on: snode, parameters: parameters)
.map2 { responseData in
// TODO: Validate this works
guard let snodePool: SnodePoolResponse = try? JSONDecoder().decode(SnodePoolResponse.self, from: responseData) else {
throw SnodeAPIError.snodePoolUpdatingFailed
}

View File

@ -258,7 +258,6 @@ public final class Storage {
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
try SSKDefaultKeychainStorage.shared.set(data: keySpec, service: keychainService, key: dbCipherKeySpecKey)
print("RAWR new keySpec generated and saved")
return keySpec
}
catch {

View File

@ -2,6 +2,7 @@
import UIKit
import GRDB
import YYImage
import SessionUIKit
import SessionMessagingKit
@ -112,8 +113,8 @@ public final class ProfilePictureView: UIView {
guard !publicKey.isEmpty || openGroupProfilePicture != nil else { return }
func getProfilePicture(of size: CGFloat, for publicKey: String, profile: Profile?) -> (image: UIImage, isTappable: Bool) {
if let profile: Profile = profile, let profilePicture: UIImage = ProfileManager.profileAvatar(profile: profile) {
return (profilePicture, true)
if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile), let image: YYImage = YYImage(data: profileData) {
return (image, true)
}
return (
@ -179,7 +180,7 @@ public final class ProfilePictureView: UIView {
hasTappableProfilePicture = isTappable
}
imageView.contentMode = .scaleAspectFit
imageView.contentMode = .scaleAspectFill
imageView.backgroundColor = Colors.unimportant
imageView.layer.cornerRadius = (targetSize / 2)
additionalImageView.layer.cornerRadius = (targetSize / 2)
@ -187,11 +188,11 @@ public final class ProfilePictureView: UIView {
// MARK: - Convenience
private func getImageView() -> UIImageView {
let result = UIImageView()
private func getImageView() -> YYAnimatedImageView {
let result = YYAnimatedImageView()
result.layer.masksToBounds = true
result.backgroundColor = Colors.unimportant
result.contentMode = .scaleAspectFit
result.contentMode = .scaleAspectFill
return result
}