diff --git a/.gitignore b/.gitignore index 21c390b4b..36979ca27 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ DerivedData *.ipa *.xcuserstate Index/ +Session-Turn-Server # CocoaPods Pods diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 65323a6ce..eb3fd9e22 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -5049,7 +5049,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 306; + CURRENT_PROJECT_VERSION = 312; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5074,7 +5074,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.18; + MARKETING_VERSION = 1.11.19; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5122,7 +5122,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 306; + CURRENT_PROJECT_VERSION = 312; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5152,7 +5152,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.18; + MARKETING_VERSION = 1.11.19; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5188,7 +5188,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 306; + CURRENT_PROJECT_VERSION = 312; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5211,7 +5211,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.18; + MARKETING_VERSION = 1.11.19; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -5262,7 +5262,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 306; + CURRENT_PROJECT_VERSION = 312; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5290,7 +5290,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.18; + MARKETING_VERSION = 1.11.19; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6198,7 +6198,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 306; + CURRENT_PROJECT_VERSION = 312; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6237,7 +6237,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.11.18; + MARKETING_VERSION = 1.11.19; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -6269,7 +6269,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 306; + CURRENT_PROJECT_VERSION = 312; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6308,7 +6308,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.11.18; + MARKETING_VERSION = 1.11.19; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 90807940f..809d9f165 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -762,6 +762,8 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc requestMicrophonePermissionIfNeeded() { [weak self] in self?.cancelVoiceMessageRecording() } + // Keep screen on + UIApplication.shared.isIdleTimerDisabled = false guard AVAudioSession.sharedInstance().recordPermission == .granted else { return } // Cancel any current audio playback audioPlayer?.stop() @@ -810,6 +812,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } func endVoiceMessageRecording() { + UIApplication.shared.isIdleTimerDisabled = true // Hide the UI snInputView.hideVoiceMessageUI() // Cancel the timer diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 7c5abfffd..b2d643011 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -356,6 +356,23 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv self.present(alert, animated: true, completion: nil) } delete.backgroundColor = Colors.destructive + + let isPinned = thread.isPinned + let pin = UITableViewRowAction(style: .normal, title: NSLocalizedString("PIN_BUTTON_TEXT", comment: "")) { [weak self] _, _ in + thread.isPinned = true + thread.save() + self?.threadViewModelCache.removeValue(forKey: thread.uniqueId!) + tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade) + } + pin.backgroundColor = Colors.pathsBuilding + let unpin = UITableViewRowAction(style: .normal, title: NSLocalizedString("UNPIN_BUTTON_TEXT", comment: "")) { [weak self] _, _ in + thread.isPinned = false + thread.save() + self?.threadViewModelCache.removeValue(forKey: thread.uniqueId!) + tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade) + } + unpin.backgroundColor = Colors.pathsBuilding + if let thread = thread as? TSContactThread { let publicKey = thread.contactSessionID() let blockingManager = SSKEnvironment.shared.blockingManager @@ -370,9 +387,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade) } unblock.backgroundColor = Colors.unimportant - return [ delete, (isBlocked ? unblock : block) ] + return [ delete, (isBlocked ? unblock : block), (isPinned ? unpin : pin) ] } else { - return [ delete ] + return [ delete, (isPinned ? unpin : pin) ] } } diff --git a/Session/Media Viewing & Editing/PhotoCapture.swift b/Session/Media Viewing & Editing/PhotoCapture.swift index 6c0eda63b..64d7ed650 100644 --- a/Session/Media Viewing & Editing/PhotoCapture.swift +++ b/Session/Media Viewing & Editing/PhotoCapture.swift @@ -593,7 +593,11 @@ class PhotoCaptureOutputAdaptee: NSObject, ImageCaptureOutput { @available(iOS 11.0, *) func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { - let data = photo.fileDataRepresentation()! + var data = photo.fileDataRepresentation()! + // Call normalized here to fix the orientation + if let srcImage = UIImage(data: data) { + data = srcImage.normalized().jpegData(compressionQuality: 1.0)! + } DispatchQueue.main.async { self.delegate?.captureOutputDidFinishProcessing(photoData: data, error: error) } diff --git a/Session/Meta/AppDelegate.m b/Session/Meta/AppDelegate.m index 413f90059..cc8ffc692 100644 --- a/Session/Meta/AppDelegate.m +++ b/Session/Meta/AppDelegate.m @@ -740,7 +740,6 @@ static NSTimeInterval launchStartedAt; - (LKAppMode)getCurrentAppMode { LKAppMode appMode = [self getAppModeOrSystemDefault]; - UIWindow *window = UIApplication.sharedApplication.keyWindow; return appMode; } diff --git a/Session/Meta/Images.xcassets/Session/Pin.imageset/Contents.json b/Session/Meta/Images.xcassets/Session/Pin.imageset/Contents.json new file mode 100644 index 000000000..ed8e0a0bf --- /dev/null +++ b/Session/Meta/Images.xcassets/Session/Pin.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Session/Meta/Images.xcassets/Session/Pin.imageset/pin.pdf b/Session/Meta/Images.xcassets/Session/Pin.imageset/pin.pdf new file mode 100644 index 000000000..bda213589 Binary files /dev/null and b/Session/Meta/Images.xcassets/Session/Pin.imageset/pin.pdf differ diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 3e0abe961..a58f90996 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -572,7 +572,10 @@ "DISMISS_BUTTON_TEXT" = "Dismiss"; /* Button text which opens the settings app */ "OPEN_SETTINGS_BUTTON" = "Settings"; -"APN_Message" = "You've got a new message"; +"APN_Message" = "You've got a new message."; +"APN_Collapsed_Messages" = "You've got %@ new messages."; "system_mode_theme" = "System"; "dark_mode_theme" = "Dark"; "light_mode_theme" = "Light"; +"PIN_BUTTON_TEXT" = "Pin"; +"UNPIN_BUTTON_TEXT" = "Unpin"; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 4b3b9f04b..52eae7ed1 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -37,6 +37,7 @@ struct AppNotificationUserInfoKey { static let threadId = "Signal.AppNotificationsUserInfoKey.threadId" static let callBackNumber = "Signal.AppNotificationsUserInfoKey.callBackNumber" static let localCallId = "Signal.AppNotificationsUserInfoKey.localCallId" + static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter" } extension AppNotificationCategory { @@ -80,9 +81,9 @@ extension AppNotificationAction { } } -// Delay notification of incoming messages when it's likely to be read by a linked device to -// avoid notifying a user on their phone while a conversation is actively happening on desktop. -let kNotificationDelayForRemoteRead: TimeInterval = 5 +// Delay notification of incoming messages when it's a background polling to +// avoid too many notifications fired at the same time +let kNotificationDelayForBackgroumdPoll: TimeInterval = 5 let kAudioNotificationsThrottleCount = 2 let kAudioNotificationsThrottleInterval: TimeInterval = 5 @@ -157,9 +158,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) { - guard !thread.isMuted else { - return - } + guard !thread.isMuted else { return } + guard let threadId = thread.uniqueId else { return } + + let identifier: String = incomingMessage.notificationIdentifier ?? UUID().uuidString + + let isBackgroudPoll = identifier == threadId // While batch processing, some of the necessary changes have not been commited. let rawMessageText = incomingMessage.previewText(with: transaction) @@ -194,13 +198,13 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { if groupName.count < 1 { groupName = MessageStrings.newGroupDefaultTitle } - notificationTitle = String(format: NotificationStrings.incomingGroupMessageTitleFormat, - senderName, - groupName) + notificationTitle = isBackgroudPoll ? groupName : String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName) default: owsFailDebug("unexpected thread: \(thread)") return } + default: + notificationTitle = "Session" } var notificationBody: String? @@ -209,24 +213,19 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { notificationBody = NotificationStrings.incomingMessageBody case .namePreview: notificationBody = messageText - } - - guard let threadId = thread.uniqueId else { - owsFailDebug("threadId was unexpectedly nil") - return + default: + notificationBody = NotificationStrings.incomingMessageBody } assert((notificationBody ?? notificationTitle) != nil) // Don't reply from lockscreen if anyone in this conversation is // "no longer verified". - var category = AppNotificationCategory.incomingMessage + let category = AppNotificationCategory.incomingMessage let userInfo = [ AppNotificationUserInfoKey.threadId: threadId ] - - let identifier: String = incomingMessage.notificationIdentifier ?? UUID().uuidString DispatchQueue.main.async { notificationBody = MentionUtilities.highlightMentions(in: notificationBody!, threadID: thread.uniqueId!) @@ -247,6 +246,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { notificationTitle = nil case .nameNoPreview, .namePreview: notificationTitle = thread.name() + default: + notificationTitle = nil } let notificationBody = NotificationStrings.failedToSendBody diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift index 13c648fe6..e5997f9a9 100644 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ b/Session/Notifications/UserNotificationsAdaptee.swift @@ -96,17 +96,18 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { let content = UNMutableNotificationContent() content.categoryIdentifier = category.identifier content.userInfo = userInfo + let isReplacingNotification = replacingIdentifier != nil + var isBackgroudPoll = false + if let threadIdentifier = userInfo[AppNotificationUserInfoKey.threadId] as? String { + content.threadIdentifier = threadIdentifier + isBackgroudPoll = replacingIdentifier == threadIdentifier + } let isAppActive = UIApplication.shared.applicationState == .active if let sound = sound, sound != OWSSound.none { content.sound = sound.notificationSound(isQuiet: isAppActive) } - - var notificationIdentifier: String = UUID().uuidString - if let replacingIdentifier = replacingIdentifier { - notificationIdentifier = replacingIdentifier - Logger.debug("replacing notification with identifier: \(notificationIdentifier)") - cancelNotification(identifier: notificationIdentifier) - } + + let notificationIdentifier = isReplacingNotification ? replacingIdentifier! : UUID().uuidString if shouldPresentNotification(category: category, userInfo: userInfo) { if let displayableTitle = title?.filterForDisplay { @@ -119,10 +120,26 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { // Play sound and vibrate, but without a `body` no banner will show. Logger.debug("supressing notification body") } + + let trigger: UNNotificationTrigger? + if isBackgroudPoll { + trigger = UNTimeIntervalNotificationTrigger(timeInterval: kNotificationDelayForBackgroumdPoll, repeats: false) + let numberOfNotifications: Int + if let lastRequest = notifications[notificationIdentifier], let counter = lastRequest.content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] as? Int { + numberOfNotifications = counter + 1 + content.body = String(format: NotificationStrings.incomingCollapsedMessagesBody, "\(numberOfNotifications)") + } else { + numberOfNotifications = 1 + } + content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] = numberOfNotifications + } else { + trigger = nil + } - let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: nil) + let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger) Logger.debug("presenting notification with identifier: \(notificationIdentifier)") + if isReplacingNotification { cancelNotification(identifier: notificationIdentifier) } notificationCenter.add(request) notifications[notificationIdentifier] = request } diff --git a/Session/Shared/BaseVC.swift b/Session/Shared/BaseVC.swift index ff53996b3..83f9c9262 100644 --- a/Session/Shared/BaseVC.swift +++ b/Session/Shared/BaseVC.swift @@ -90,7 +90,9 @@ class BaseVC : UIViewController { } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - NotificationCenter.default.post(name: .appModeChanged, object: nil) + if LKAppModeUtilities.isSystemDefault { + NotificationCenter.default.post(name: .appModeChanged, object: nil) + } } @objc internal func handleAppModeChangedNotification(_ notification: Notification) { diff --git a/Session/Shared/ConversationCell.swift b/Session/Shared/ConversationCell.swift index 6f8a23afe..2a7edf38a 100644 --- a/Session/Shared/ConversationCell.swift +++ b/Session/Shared/ConversationCell.swift @@ -58,6 +58,17 @@ final class ConversationCell : UITableViewCell { return result }() + private lazy var isPinnedIcon: UIImageView = { + let result = UIImageView(image: UIImage(named: "Pin")!.withRenderingMode(.alwaysTemplate)) + result.contentMode = .scaleAspectFit + let size = ConversationCell.unreadCountViewSize + result.set(.width, to: size) + result.set(.height, to: size) + result.tintColor = Colors.pinIcon + result.layer.masksToBounds = true + return result + }() + private lazy var timestampLabel: UILabel = { let result = UILabel() result.font = .systemFont(ofSize: Values.smallFontSize) @@ -124,7 +135,7 @@ final class ConversationCell : UITableViewCell { hasMentionLabel.pin(to: hasMentionView) // Label stack view let topLabelSpacer = UIView.hStretchingSpacer() - let topLabelStackView = UIStackView(arrangedSubviews: [ displayNameLabel, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ]) + let topLabelStackView = UIStackView(arrangedSubviews: [ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ]) topLabelStackView.axis = .horizontal topLabelStackView.alignment = .center topLabelStackView.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer @@ -182,6 +193,7 @@ final class ConversationCell : UITableViewCell { private func update() { AssertIsOnMainThread() guard let thread = threadViewModel?.threadRecord else { return } + backgroundColor = threadViewModel.isPinned ? Colors.cellPinned : Colors.cellBackground let isBlocked: Bool if let thread = thread as? TSContactThread { isBlocked = SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(thread.contactSessionID()) @@ -195,6 +207,7 @@ final class ConversationCell : UITableViewCell { accentLineView.backgroundColor = Colors.accent accentLineView.alpha = threadViewModel.hasUnreadMessages ? 1 : 0.0001 // Setting the alpha to exactly 0 causes an issue on iOS 12 } + isPinnedIcon.isHidden = !threadViewModel.isPinned unreadCountView.isHidden = !threadViewModel.hasUnreadMessages let unreadCount = threadViewModel.unreadCount unreadCountLabel.text = unreadCount < 100 ? "\(unreadCount)" : "99+" diff --git a/SessionMessagingKit/Database/Storage+Messaging.swift b/SessionMessagingKit/Database/Storage+Messaging.swift index e02363de7..0035217f2 100644 --- a/SessionMessagingKit/Database/Storage+Messaging.swift +++ b/SessionMessagingKit/Database/Storage+Messaging.swift @@ -28,6 +28,7 @@ extension Storage { let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return nil } let tsMessage: TSMessage if message.sender == getUserPublicKey() { + if let _ = TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) { return nil } let tsOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction) var recipients: [String] = [] if let syncTarget = message.syncTarget { diff --git a/SessionMessagingKit/Database/TSDatabaseView.m b/SessionMessagingKit/Database/TSDatabaseView.m index 4668aeb44..d899289e4 100644 --- a/SessionMessagingKit/Database/TSDatabaseView.m +++ b/SessionMessagingKit/Database/TSDatabaseView.m @@ -278,7 +278,10 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup" TSThread *thread1 = (TSThread *)object1; TSThread *thread2 = (TSThread *)object2; if ([group isEqualToString:TSArchiveGroup] || [group isEqualToString:TSInboxGroup]) { - + if (thread1.isPinned != thread2.isPinned) { + if (thread1.isPinned) { return NSOrderedDescending; } + if (thread2.isPinned) { return NSOrderedAscending; } + } TSInteraction *_Nullable lastInteractionForInbox1 = [thread1 lastInteractionForInboxWithTransaction:transaction]; NSDate *lastInteractionForInboxDate1 = lastInteractionForInbox1 ? lastInteractionForInbox1.receivedAtDate : thread1.creationDate; diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 5e9f987d9..dd44b29b0 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -342,7 +342,9 @@ 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) + // Use the same identifier for notifications when in backgroud polling to prevent spam + let notificationIdentifier = isBackgroundPoll ? thread.uniqueId : UUID().uuidString + tsIncomingMessage.setNotificationIdentifier(notificationIdentifier, transaction: transaction) DispatchQueue.main.async { Storage.read { transaction in SSKEnvironment.shared.notificationsManager!.notifyUser(for: tsIncomingMessage, in: thread, transaction: transaction) diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index d32b07681..e9042b81f 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -116,8 +116,6 @@ public final class MessageSender : NSObject { if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set message.sentTimestamp = NSDate.millisecondTimestamp() } - // Ignore future self-sends - Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp!, using: transaction) message.sender = userPublicKey switch destination { case .contact(let publicKey): message.recipient = publicKey @@ -268,8 +266,6 @@ public final class MessageSender : NSObject { if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set message.sentTimestamp = NSDate.millisecondTimestamp() } - // Ignore future self-sends - Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp!, using: transaction) message.sender = storage.getUserPublicKey() switch destination { case .contact(_): preconditionFailure() @@ -366,7 +362,11 @@ public final class MessageSender : NSObject { let userPublicKey = getUserHexEncodedPublicKey() if case .contact(let publicKey) = destination, !isSyncMessage { if let message = message as? VisibleMessage { message.syncTarget = publicKey } - if let message = message as? ExpirationTimerUpdate { message.syncTarget = publicKey } + if let message = message as? ExpirationTimerUpdate { + message.syncTarget = publicKey + // Prevent the same ExpirationTimerUpdate to be handled twice + Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp!, using: transaction) + } // FIXME: Make this a job sendToSnodeDestination(.contact(publicKey: userPublicKey), message: message, using: transaction, isSyncMessage: true).retainUntilComplete() } diff --git a/SessionMessagingKit/Threads/TSThread.h b/SessionMessagingKit/Threads/TSThread.h index aa333d245..c2449397a 100644 --- a/SessionMessagingKit/Threads/TSThread.h +++ b/SessionMessagingKit/Threads/TSThread.h @@ -16,6 +16,7 @@ BOOL IsNoteToSelfEnabled(void); */ @interface TSThread : TSYapDatabaseObject +@property (nonatomic) BOOL isPinned; @property (nonatomic) BOOL shouldBeVisible; @property (nonatomic, readonly) NSDate *creationDate; @property (nonatomic, readonly, nullable) NSDate *lastInteractionDate; diff --git a/SessionUIKit/Style Guide/Colors.swift b/SessionUIKit/Style Guide/Colors.swift index 4d25e7cc5..1a150ebc9 100644 --- a/SessionUIKit/Style Guide/Colors.swift +++ b/SessionUIKit/Style Guide/Colors.swift @@ -20,6 +20,7 @@ public final class Colors : NSObject { @objc public static var border: UIColor { UIColor(named: "session_border")! } @objc public static var cellBackground: UIColor { UIColor(named: "session_cell_background")! } @objc public static var cellSelected: UIColor { UIColor(named: "session_cell_selected")! } + @objc public static var cellPinned: UIColor { UIColor(named: "session_cell_pinned")! } @objc public static var navigationBarBackground: UIColor { UIColor(named: "session_navigation_bar_background")! } @objc public static var searchBarPlaceholder: UIColor { UIColor(named: "session_search_bar_placeholder")! } // Also used for the icons @objc public static var searchBarBackground: UIColor { UIColor(named: "session_search_bar_background")! } @@ -40,4 +41,5 @@ public final class Colors : NSObject { @objc public static var pnOptionBackground: UIColor { UIColor(named: "session_pn_option_background")! } @objc public static var pnOptionBorder: UIColor { UIColor(named: "session_pn_option_border")! } @objc public static var pathsBuilding: UIColor { UIColor(named: "session_paths_building")! } + @objc public static var pinIcon: UIColor { UIColor(named: "session_pin_icon")! } } diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_cell_pinned.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_cell_pinned.colorset/Contents.json new file mode 100644 index 000000000..a5a409cf4 --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_cell_pinned.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF0", + "green" : "0xF0", + "red" : "0xF0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x40", + "green" : "0x40", + "red" : "0x40" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_pin_icon.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_pin_icon.colorset/Contents.json new file mode 100644 index 000000000..dea6b5f39 --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_pin_icon.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "96", + "green" : "96", + "red" : "96" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "179", + "green" : "179", + "red" : "179" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift index 92e4f93c1..12fae43f5 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift @@ -166,7 +166,7 @@ public class ImageEditorCanvasView: UIView { // of code simplicity. We could modify the image layer's // transform to handle the normalization, which would // have perf benefits. - return srcImage.normalized() + return srcImage } // MARK: - Content diff --git a/SignalUtilitiesKit/Messaging/ThreadViewModel.swift b/SignalUtilitiesKit/Messaging/ThreadViewModel.swift index 0162b873f..2e6f9bab1 100644 --- a/SignalUtilitiesKit/Messaging/ThreadViewModel.swift +++ b/SignalUtilitiesKit/Messaging/ThreadViewModel.swift @@ -14,6 +14,7 @@ public class ThreadViewModel: NSObject { @objc public let contactSessionID: String? @objc public let name: String @objc public let isMuted: Bool + @objc public let isPinned: Bool @objc public let isOnlyNotifyingForMentions: Bool @objc public let hasUnreadMentions: Bool @@ -31,6 +32,7 @@ public class ThreadViewModel: NSObject { self.isGroupThread = thread.isGroupThread() self.name = thread.name() self.isMuted = thread.isMuted + self.isPinned = thread.isPinned self.lastMessageText = thread.lastMessageText(transaction: transaction) let lastInteraction = thread.lastInteractionForInbox(transaction: transaction) self.lastMessageForInbox = lastInteraction diff --git a/SignalUtilitiesKit/Utilities/CommonStrings.swift b/SignalUtilitiesKit/Utilities/CommonStrings.swift index a5e4649d3..2d7199593 100644 --- a/SignalUtilitiesKit/Utilities/CommonStrings.swift +++ b/SignalUtilitiesKit/Utilities/CommonStrings.swift @@ -50,6 +50,9 @@ public class NotificationStrings: NSObject { @objc static public let incomingMessageBody = NSLocalizedString("APN_Message", comment: "notification body") + + @objc + static public let incomingCollapsedMessagesBody = NSLocalizedString("APN_Collapsed_Messages", comment: "collapsed notification body for background polling") @objc static public let incomingGroupMessageTitleFormat = NSLocalizedString("NEW_GROUP_MESSAGE_NOTIFICATION_TITLE", comment: "notification title. Embeds {{author name}} and {{group name}}")