Built out the Message Request functionality

Added the MessageRequestsViewController
Added a 'Message Requests' button to the settings screen
Added accept/reject buttons for message requests to the ConversationVC
Added the ability to hide the message request item on the HomeVC (re-appears if you get a new message request)
Added code to handle an edge-case where the message request approval state wouldn't be returned to the sender due to the recipient running an old version of the app
Prevented contacts which aren't associated with an approved thread from appearing when creating a closed group
Disabled notifications for threads which aren't approved
Updated the app notification count to exclude unapproved messages
Updated the app to ignore closed group creation messages if the group has no admins which are approved contacts
Fixed up the keyboard avoidance behaviour in the ConversationVC
Fixed a couple of minor interaction issues which affected some devices
Fixed an issue where the database migrations would run on the 2nd launch when creating a new account (causing odd behaviours)
This commit is contained in:
Morgan Pretty 2022-02-02 16:59:56 +11:00
parent 0f20c37afa
commit 9db5083cc5
66 changed files with 2480 additions and 306 deletions

View File

@ -679,7 +679,6 @@
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */; };
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */; };
C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; };
C3AAFFE825AE975D0089E6DD /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */; };
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; };
C3ADC66126426688005F1414 /* ShareVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareVC.swift */; };
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; };
@ -778,6 +777,12 @@
F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; };
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; };
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */; };
FD659AC227A7857C00F12C02 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659AC127A7857C00F12C02 /* UIColor+Extensions.swift */; };
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -1117,10 +1122,10 @@
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = "<group>"; };
7BA6F47DAD18D44D75B7110F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; sourceTree = "<group>"; };
7BA7F4BA279F9F5800B3A466 /* EmptySearchResultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptySearchResultCell.swift; sourceTree = "<group>"; };
7BA7F4BC27A216B600B3A466 /* Storage+RecentSearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+RecentSearchResults.swift"; sourceTree = "<group>"; };
7BA9057D27911C5800998B3C /* GlobalSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchViewController.swift; sourceTree = "<group>"; };
7BA6F47DAD18D44D75B7110F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; sourceTree = "<group>"; };
7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -1703,7 +1708,6 @@
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMissingModal.swift; sourceTree = "<group>"; };
C3A8AF752665B03900A467FE /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; };
C3A8AF762665F97A00A467FE /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C3ADC66026426688005F1414 /* ShareVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVC.swift; sourceTree = "<group>"; };
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1806,6 +1810,12 @@
F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
FD659AC127A7857C00F12C02 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
@ -2447,7 +2457,9 @@
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */,
C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */,
C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */,
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */,
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */,
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */,
);
path = "Control Messages";
sourceTree = "<group>";
@ -2815,9 +2827,11 @@
C360968E25AD16E8008B62B2 /* Home */ = {
isa = PBXGroup;
children = (
7BA7F4B9279F9F3700B3A466 /* GlobalSearch */,
FD659ABE27A7648200F12C02 /* Message Requests */,
FD88BAD727A7438E00BBC442 /* Views */,
B8BB82A4238F627000BA5194 /* HomeVC.swift */,
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
7BA7F4B9279F9F3700B3A466 /* GlobalSearch */,
);
path = Home;
sourceTree = "<group>";
@ -3031,6 +3045,7 @@
isa = PBXGroup;
children = (
B8B32044258C117C0020074B /* ContactsMigration.swift */,
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */,
@ -3135,7 +3150,6 @@
C38EF2E3255B6DB9007E1867 /* OWSUnreadIndicator.m */,
C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */,
C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */,
C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */,
);
path = Messaging;
sourceTree = "<group>";
@ -3372,6 +3386,7 @@
C38EF236255B6D65007E1867 /* UIViewController+OWS.h */,
C38EF23B255B6D66007E1867 /* UIViewController+OWS.m */,
C38EF23C255B6D66007E1867 /* UIColor+OWS.h */,
FD659AC127A7857C00F12C02 /* UIColor+Extensions.swift */,
C38EF242255B6D67007E1867 /* UIColor+OWS.m */,
C38EF2B2255B6D9C007E1867 /* UIView+Utilities.swift */,
C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */,
@ -3597,6 +3612,22 @@
path = Session;
sourceTree = "<group>";
};
FD659ABE27A7648200F12C02 /* Message Requests */ = {
isa = PBXGroup;
children = (
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */,
);
path = "Message Requests";
sourceTree = "<group>";
};
FD88BAD727A7438E00BBC442 /* Views */ = {
isa = PBXGroup;
children = (
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */,
);
path = Views;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@ -4475,6 +4506,7 @@
C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */,
C3D90A7A25773A93002C9DF5 /* Configuration.swift in Sources */,
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */,
FD659AC227A7857C00F12C02 /* UIColor+Extensions.swift in Sources */,
C33FDC64255A582000E217F9 /* NSObject+Casting.m in Sources */,
C38EF388255B6DD2007E1867 /* AttachmentApprovalViewController.swift in Sources */,
C33FDD53255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.m in Sources */,
@ -4547,13 +4579,13 @@
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */,
C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */,
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */,
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */,
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */,
B8C2B332256376F000551B4D /* ThreadUtil.m in Sources */,
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */,
C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */,
C3AAFFE825AE975D0089E6DD /* ConfigurationMessage+Convenience.swift in Sources */,
C38EF359255B6DCC007E1867 /* SheetViewController.swift in Sources */,
B8F5F52925EC4F8A003BF8D4 /* BlockListUIUtils.m in Sources */,
C38EF386255B6DD2007E1867 /* AttachmentApprovalInputAccessoryView.swift in Sources */,
@ -4664,6 +4696,7 @@
C300A5F22554B09800555489 /* MessageSender.swift in Sources */,
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */,
C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */,
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */,
C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */,
C32A026325A801AA000ED5D4 /* NSData+messagePadding.m in Sources */,
C352A3932557883D00338F3E /* JobDelegate.swift in Sources */,
@ -4722,6 +4755,7 @@
C32C5C4F256DCC36003C73A2 /* Storage+OpenGroups.swift in Sources */,
C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */,
B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */,
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */,
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */,
@ -4859,11 +4893,13 @@
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */,
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */,
B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */,
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */,
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */,
45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */,
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */,
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
B8214A2B25D63EB9009C0F2A /* MessagesTableView.swift in Sources */,

View File

@ -212,9 +212,12 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
func sendMessage(hasPermissionToSendSeed: Bool = false) {
guard !showBlockedModalIfNeeded() else { return }
let text = replaceMentions(in: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines))
let thread = self.thread
guard !text.isEmpty else { return }
if text.contains(mnemonic) && !thread.isNoteToSelf() && !hasPermissionToSendSeed {
// Warn the user if they're about to send their seed to someone
let modal = SendSeedModal()
@ -223,28 +226,55 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
modal.proceed = { self.sendMessage(hasPermissionToSendSeed: true) }
return present(modal, animated: true, completion: nil)
}
let message = VisibleMessage()
message.sentTimestamp = NSDate.millisecondTimestamp()
let sentTimestamp: UInt64 = NSDate.millisecondTimestamp()
let message: VisibleMessage = VisibleMessage()
message.sentTimestamp = sentTimestamp
message.text = text
message.quote = VisibleMessage.Quote.from(snInputView.quoteDraftInfo?.model)
// Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can
// use it to determine if the user is creating a new thread and update the 'isApproved'
// flags appropriately
let oldThreadShouldBeVisible: Bool = thread.shouldBeVisible
let linkPreviewDraft = snInputView.linkPreviewInfo?.draft
let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
viewModel.appendUnsavedOutgoingTextMessage(tsMessage)
Storage.write(with: { transaction in
message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction)
}, completion: { [weak self] in
tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
Storage.shared.write(with: { transaction in
tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
}, completion: { [weak self] in
// At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing
// the height of the new message cell
self?.scrollToBottom(isAnimated: false)
})
Storage.shared.write { transaction in
MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
}
self?.handleMessageSent()
Storage.shared.write(
with: { transaction in
tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction)
},
completion: { [weak self] in
// At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing
// the height of the new message cell
self?.scrollToBottom(isAnimated: false)
}
)
Storage.shared.write(
with: { transaction in
self?.approveMessageRequestIfNeeded(
for: self?.thread,
with: (transaction as! YapDatabaseReadWriteTransaction),
isNewThread: !oldThreadShouldBeVisible,
timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting
)
},
completion: { [weak self] in
Storage.shared.write { transaction in
MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction)
}
self?.handleMessageSent()
}
)
})
}
@ -981,3 +1011,99 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
OWSAlerts.showAlert(title: title, message: message)
}
}
// MARK: - Message Request Actions
extension ConversationVC {
fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) {
guard let contactThread: TSContactThread = thread as? TSContactThread else { return }
// If the contact doesn't exist then we should create it so we can store the 'isApproved' state
// (it'll be updated with correct profile info if they accept the message request so this
// shouldn't cause weird behaviours)
let sessionId: String = contactThread.contactSessionID()
let contact: Contact = (Storage.shared.getContact(with: sessionId) ?? Contact(sessionID: sessionId))
if !contact.isApproved {
// Default 'didApproveMe' to true for the person approving the message request
contact.isApproved = true
contact.didApproveMe = (contact.didApproveMe || !isNewThread)
Storage.shared.setContact(contact, using: transaction)
// If we aren't creating a new thread (ie. sending a message request) then send a
// messageRequestResponse back to the sender (this allows the sender to know that
// they have been approved and can now use this contact in closed groups)
if !isNewThread {
let messageRequestResponse: MessageRequestResponse = MessageRequestResponse(
publicKey: sessionId,
isApproved: true
)
messageRequestResponse.sentTimestamp = timestamp
MessageSender.send(messageRequestResponse, in: contactThread, using: transaction)
}
// Hide the 'messageRequestView' since the request has been approved and force a config
// sync to propagate the contact approval state (both must run on the main thread)
DispatchQueue.main.async { [weak self] in
self?.messageRequestView.isHidden = true
// Send a sync message with the details of the contact
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
}
}
}
}
@objc func acceptMessageRequest() {
Storage.write { [weak self] transaction in
self?.approveMessageRequestIfNeeded(
for: self?.thread,
with: transaction,
isNewThread: false,
timestamp: NSDate.millisecondTimestamp()
)
}
}
@objc func deleteMessageRequest() {
guard let uniqueId: String = thread.uniqueId else { return }
Storage.write(
with: { [weak self] transaction in
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
// Update the contact
if let contactThread: TSContactThread = self?.thread as? TSContactThread {
let sessionId: String = contactThread.contactSessionID()
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
contact.isApproved = false
contact.isBlocked = true
Storage.shared.setContact(contact, using: transaction)
}
}
// Delete all thread content
self?.thread.removeAllThreadInteractions(with: transaction)
self?.thread.remove(with: transaction)
},
completion: { [weak self] in
// Block the contact
if let sessionId: String = (self?.thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
}
// Force a config sync and pop to the previous screen (both must run on the main thread)
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
}
self?.navigationController?.popViewController(animated: true)
}
}
)
}
}

View File

@ -1,3 +1,4 @@
import SessionUIKit
// TODO:
// Slight paging glitch when scrolling up and loading more content
@ -11,6 +12,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
var focusedMessageIndexPath: IndexPath?
var unreadViewItems: [ConversationViewItem] = []
var scrollButtonConstraint: NSLayoutConstraint?
var footerControlsStackViewBottomConstraint: NSLayoutConstraint?
// Search
var isShowingSearchUI = false
var lastSearchedText: String?
@ -93,7 +95,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
return result
}()
// MARK: UI Components
// MARK: - UI
private static let messageRequestButtonHeight: CGFloat = 34
lazy var titleView: ConversationTitleView = {
let result = ConversationTitleView(thread: thread)
result.delegate = self
@ -101,10 +106,18 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}()
lazy var messagesTableView: MessagesTableView = {
let result = MessagesTableView()
result.dataSource = self
result.delegate = self
return result
let tableView: MessagesTableView = MessagesTableView()
tableView.dataSource = self
tableView.delegate = self
tableView.contentInsetAdjustmentBehavior = .never
tableView.contentInset = UIEdgeInsets(
top: 0,
leading: 0,
bottom: Values.mediumSpacing,
trailing: 0
)
return tableView
}()
lazy var snInputView = InputView(delegate: self)
@ -128,8 +141,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
return result
}()
lazy var scrollButton = ScrollToBottomButton(delegate: self)
lazy var blockedBanner: InfoBanner = {
let name: String
if let thread = thread as? TSContactThread {
@ -146,6 +157,104 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
return result
}()
lazy var footerControlsStackView: UIStackView = {
let stackView: UIStackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .trailing
stackView.distribution = .equalSpacing
stackView.spacing = 10
stackView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
stackView.isLayoutMarginsRelativeArrangement = true
return stackView
}()
lazy var scrollButton = ScrollToBottomButton(delegate: self)
lazy var messageRequestView: UIView = {
let view: UIView = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = !thread.isMessageRequest()
view.setGradient(Gradients.defaultBackground)
return view
}()
private let messageRequestDescriptionLabel: UILabel = {
let label: UILabel = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 12)
label.text = NSLocalizedString("MESSAGE_REQUESTS_INFO", comment: "")
label.textColor = Colors.sessionMessageRequestsInfoText
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
private let messageRequestAcceptButton: UIButton = {
let button: UIButton = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.clipsToBounds = true
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitle(NSLocalizedString("TXT_DELETE_ACCEPT", comment: ""), for: .normal)
button.setTitleColor(Colors.sessionHeading, for: .normal)
button.setBackgroundImage(
Colors.sessionHeading
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
.toImage(isDarkMode: isDarkMode),
for: .highlighted
)
button.layer.cornerRadius = (ConversationVC.messageRequestButtonHeight / 2)
button.layer.borderColor = {
if #available(iOS 13.0, *) {
return Colors.sessionHeading
.resolvedColor(
// Note: This is needed for '.cgColor' to support dark mode
with: UITraitCollection(userInterfaceStyle: isDarkMode ? .dark : .light)
).cgColor
}
return Colors.sessionHeading.cgColor
}()
button.layer.borderWidth = 1
button.addTarget(self, action: #selector(acceptMessageRequest), for: .touchUpInside)
return button
}()
private let messageRequestDeleteButton: UIButton = {
let button: UIButton = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.clipsToBounds = true
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitle(NSLocalizedString("TXT_DELETE_TITLE", comment: ""), for: .normal)
button.setTitleColor(Colors.destructive, for: .normal)
button.setBackgroundImage(
Colors.destructive
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
.toImage(isDarkMode: isDarkMode),
for: .highlighted
)
button.layer.cornerRadius = (ConversationVC.messageRequestButtonHeight / 2)
button.layer.borderColor = {
if #available(iOS 13.0, *) {
return Colors.destructive
.resolvedColor(
// Note: This is needed for '.cgColor' to support dark mode
with: UITraitCollection(userInterfaceStyle: isDarkMode ? .dark : .light)
).cgColor
}
return Colors.destructive.cgColor
}()
button.layer.borderWidth = 1
button.addTarget(self, action: #selector(deleteMessageRequest), for: .touchUpInside)
return button
}()
// MARK: Settings
static let unreadCountViewSize: CGFloat = 20
/// The table view's bottom inset (content will have this distance to the bottom if the table view is fully scrolled down).
@ -187,9 +296,50 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// Constraints
view.addSubview(messagesTableView)
messagesTableView.pin(to: view)
view.addSubview(scrollButton)
scrollButton.pin(.right, to: .right, of: view, withInset: -16)
scrollButtonConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16)
// Blocked banner
addOrRemoveBlockedBanner()
// Message requests view & scroll to bottom
view.addSubview(footerControlsStackView)
scrollButton.translatesAutoresizingMaskIntoConstraints = false
footerControlsStackView.addArrangedSubview(scrollButton)
footerControlsStackView.addArrangedSubview(messageRequestView)
messageRequestView.addSubview(messageRequestDescriptionLabel)
messageRequestView.addSubview(messageRequestAcceptButton)
messageRequestView.addSubview(messageRequestDeleteButton)
let footerControlsStackViewBottomConstraint: NSLayoutConstraint = footerControlsStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16)
self.footerControlsStackViewBottomConstraint = footerControlsStackViewBottomConstraint
NSLayoutConstraint.activate([
footerControlsStackView.leftAnchor.constraint(equalTo: view.leftAnchor),
footerControlsStackView.rightAnchor.constraint(equalTo: view.rightAnchor),
footerControlsStackViewBottomConstraint,
scrollButton.rightAnchor.constraint(equalTo: footerControlsStackView.rightAnchor, constant: -20),
messageRequestView.leftAnchor.constraint(equalTo: footerControlsStackView.leftAnchor),
messageRequestView.rightAnchor.constraint(equalTo: footerControlsStackView.rightAnchor),
messageRequestDescriptionLabel.topAnchor.constraint(equalTo: messageRequestView.topAnchor, constant: 10),
messageRequestDescriptionLabel.leftAnchor.constraint(equalTo: messageRequestView.leftAnchor, constant: 40),
messageRequestDescriptionLabel.rightAnchor.constraint(equalTo: messageRequestView.rightAnchor, constant: -40),
messageRequestAcceptButton.topAnchor.constraint(equalTo: messageRequestDescriptionLabel.bottomAnchor, constant: 20),
messageRequestAcceptButton.leftAnchor.constraint(equalTo: messageRequestView.leftAnchor, constant: 20),
messageRequestAcceptButton.bottomAnchor.constraint(equalTo: messageRequestView.bottomAnchor),
messageRequestAcceptButton.heightAnchor.constraint(equalToConstant: ConversationVC.messageRequestButtonHeight),
messageRequestDeleteButton.topAnchor.constraint(equalTo: messageRequestDescriptionLabel.bottomAnchor, constant: 20),
messageRequestDeleteButton.leftAnchor.constraint(equalTo: messageRequestAcceptButton.rightAnchor, constant: 20),
messageRequestDeleteButton.rightAnchor.constraint(equalTo: messageRequestView.rightAnchor, constant: -20),
messageRequestDeleteButton.bottomAnchor.constraint(equalTo: messageRequestView.bottomAnchor),
messageRequestDeleteButton.widthAnchor.constraint(equalTo: messageRequestAcceptButton.widthAnchor),
messageRequestDeleteButton.heightAnchor.constraint(equalToConstant: ConversationVC.messageRequestButtonHeight)
])
// Unread count view
view.addSubview(unreadCountView)
unreadCountView.addSubview(unreadCountLabel)
@ -197,8 +347,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
unreadCountView.centerYAnchor.constraint(equalTo: scrollButton.topAnchor).isActive = true
unreadCountView.center(.horizontal, in: scrollButton)
updateUnreadCountView()
// Blocked banner
addOrRemoveBlockedBanner()
// Notifications
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillChangeFrameNotification(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
@ -327,24 +476,94 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
@objc func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
if (newHeight > 0 && baselineKeyboardHeight == 0) {
baselineKeyboardHeight = newHeight
self.messagesTableView.keyboardHeight = newHeight
// Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600
// and https://stackoverflow.com/a/25260930 to better understand what we are
// doing with the UIViewAnimationOptions
let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:])
let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0)
let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue))
let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16))
let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero)
// Calculate new positions (Need the ensure the 'messageRequestView' has been layed out as it's
// needed for proper calculations, so force an initial layout if it doesn't have a size)
var hasDoneLayout: Bool = true
if messageRequestView.bounds.height <= CGFloat.leastNonzeroMagnitude {
hasDoneLayout = false
UIView.performWithoutAnimation {
self.view.layoutIfNeeded()
}
}
scrollButtonConstraint?.constant = -(newHeight + 16)
let newContentOffsetY = max(self.messagesTableView.contentOffset.y + min(lastPageTop, 0) + newHeight - self.messagesTableView.keyboardHeight, 0.0)
self.messagesTableView.contentOffset.y = newContentOffsetY
self.messagesTableView.keyboardHeight = newHeight
self.scrollButton.alpha = self.getScrollButtonOpacity()
let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY)
let messageRequestsOffset: CGFloat = (messageRequestView.isHidden ? 0 : messageRequestView.bounds.height + 16)
let oldContentInset: UIEdgeInsets = messagesTableView.contentInset
let newContentInset: UIEdgeInsets = UIEdgeInsets(
top: 0,
leading: 0,
bottom: (Values.mediumSpacing + keyboardTop + messageRequestsOffset),
trailing: 0
)
let newContentOffsetY: CGFloat = (messagesTableView.contentOffset.y + (newContentInset.bottom - oldContentInset.bottom))
let changes = { [weak self] in
self?.footerControlsStackViewBottomConstraint?.constant = -(keyboardTop + 16)
self?.messagesTableView.contentInset = newContentInset
self?.messagesTableView.contentOffset.y = newContentOffsetY
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
self?.scrollButton.alpha = scrollButtonOpacity
self?.view.setNeedsLayout()
self?.view.layoutIfNeeded()
}
// Perform the changes (don't animate if the initial layout hasn't been completed)
guard hasDoneLayout else {
UIView.performWithoutAnimation {
changes()
}
return
}
UIView.animate(
withDuration: duration,
delay: 0,
options: options,
animations: changes,
completion: nil
)
}
@objc func handleKeyboardWillHideNotification(_ notification: Notification) {
self.messagesTableView.contentOffset.y -= (self.messagesTableView.keyboardHeight - self.baselineKeyboardHeight)
self.messagesTableView.keyboardHeight = self.baselineKeyboardHeight
scrollButtonConstraint?.constant = -(self.baselineKeyboardHeight + 16)
self.scrollButton.alpha = self.getScrollButtonOpacity()
self.unreadCountView.alpha = self.scrollButton.alpha
// Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600
// and https://stackoverflow.com/a/25260930 to better understand what we are
// doing with the UIViewAnimationOptions
let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:])
let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0)
let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue))
let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16))
let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero)
let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY)
UIView.animate(
withDuration: duration,
delay: 0,
options: options,
animations: { [weak self] in
self?.footerControlsStackViewBottomConstraint?.constant = -(keyboardTop + 16)
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
self?.scrollButton.alpha = scrollButtonOpacity
self?.unreadCountView.alpha = scrollButtonOpacity
self?.view.setNeedsLayout()
self?.view.layoutIfNeeded()
},
completion: nil
)
}
func conversationViewModelWillUpdate() {

View File

@ -1,22 +1,5 @@
final class MessagesTableView : UITableView {
var keyboardHeight: CGFloat = 0
// Overriding contentInset and adjustedContentInset is to keep them from changing when the
// conversation view controller is dismissed.
override var contentInset: UIEdgeInsets {
get { UIEdgeInsets(top: 0, leading: 0, bottom: MessagesTableView.baselineContentInset + keyboardHeight, trailing: 0) }
set { }
}
override var adjustedContentInset: UIEdgeInsets {
get { UIEdgeInsets(top: 0, leading: 0, bottom: MessagesTableView.baselineContentInset + keyboardHeight, trailing: 0) }
set { }
}
private static let baselineContentInset = Values.mediumSpacing
override init(frame: CGRect, style: UITableView.Style) {
super.init(frame: frame, style: style)
initialize()

View File

@ -7,6 +7,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
private var tableViewTopConstraint: NSLayoutConstraint!
private var messageRequestCount: UInt {
threads.numberOfItems(inGroup: TSMessageRequestGroup)
}
private var threadCount: UInt {
threads.numberOfItems(inGroup: TSInboxGroup)
}
@ -34,6 +38,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
let result = UITableView()
result.backgroundColor = .clear
result.separatorStyle = .none
result.register(MessageRequestsCell.self, forCellReuseIdentifier: MessageRequestsCell.reuseIdentifier)
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
@ -132,7 +137,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
// Threads (part 2)
threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup, TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
threads.setIsReversed(true, forGroup: TSInboxGroup)
dbConnection.read { transaction in
self.threads.update(with: transaction) // Perform the initial update
@ -167,18 +172,42 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
NotificationCenter.default.removeObserver(self)
}
// MARK: Table View Data Source
// MARK: - UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Int(threadCount)
switch section {
case 0:
if messageRequestCount > 0 && !UserDefaults.standard[.hasHiddenMessageRequests] {
return 1
}
return 0
case 1: return Int(threadCount)
default: return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
cell.threadViewModel = threadViewModel(at: indexPath.row)
return cell
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: MessageRequestsCell.reuseIdentifier) as! MessageRequestsCell
cell.update(with: Int(messageRequestCount))
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
cell.threadViewModel = threadViewModel(at: indexPath.row)
return cell
}
}
// MARK: Updating
private func reload() {
AssertIsOnMainThread()
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
@ -203,28 +232,94 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
let notifications = dbConnection.beginLongLivedReadTransaction()
guard !notifications.isEmpty else { return }
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
let hasChanges = ext.hasChanges(forGroup: TSInboxGroup, in: notifications)
let hasChanges = (
ext.hasChanges(forGroup: TSMessageRequestGroup, in: notifications) ||
ext.hasChanges(forGroup: TSInboxGroup, in: notifications)
)
guard hasChanges else { return }
if let firstChangeSet = notifications[0].userInfo {
let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64
// The 'getSectionChanges' code below will crash if we try to process multiple commits at once
// so just force a full reload
if threads.snapshotOfLastUpdate != firstSnapshot - 1 {
return reload() // The code below will crash if we try to process multiple commits at once
// Check if we inserted a new message request (if so then unhide the message request banner)
if
let extensions: [String: Any] = firstChangeSet[YapDatabaseExtensionsKey] as? [String: Any],
let viewExtensions: [String: Any] = extensions[TSThreadDatabaseViewExtensionName] as? [String: Any]
{
// Note: We do a 'flatMap' here rather than explicitly grab the desired key because
// the key we need is 'changeset_key_changes' in 'YapDatabaseViewPrivate.h' so could
// change due to an update and silently break this - this approach is a bit safer
let allChanges: [Any] = Array(viewExtensions.values).compactMap { $0 as? [Any] }.flatMap { $0 }
let messageRequestInserts = allChanges
.compactMap { $0 as? YapDatabaseViewRowChange }
.filter { $0.finalGroup == TSMessageRequestGroup && $0.type == .insert }
if !messageRequestInserts.isEmpty && UserDefaults.standard[.hasHiddenMessageRequests] {
UserDefaults.standard[.hasHiddenMessageRequests] = false
}
}
return reload()
}
}
var sectionChanges = NSArray()
var rowChanges = NSArray()
ext.getSectionChanges(&sectionChanges, rowChanges: &rowChanges, for: notifications, with: threads)
guard sectionChanges.count > 0 || rowChanges.count > 0 else { return }
// Separate out the changes for new message requests and the inbox (so we can avoid updating for
// new messages within an existing message request)
let messageRequestInserts = rowChanges
.compactMap { $0 as? YapDatabaseViewRowChange }
.filter { $0.finalGroup == TSMessageRequestGroup && $0.type == .insert }
let inboxRowChanges = rowChanges
.filter { ($0 as? YapDatabaseViewRowChange)?.finalGroup != TSMessageRequestGroup }
guard sectionChanges.count > 0 || inboxRowChanges.count > 0 || messageRequestInserts.count > 0 else { return }
tableView.beginUpdates()
rowChanges.forEach { rowChange in
// If we need to unhide the message request row and then re-insert it
if !messageRequestInserts.isEmpty && UserDefaults.standard[.hasHiddenMessageRequests] {
UserDefaults.standard[.hasHiddenMessageRequests] = false
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
}
// TODO: Crash due to change from Message Requests getting approved?
inboxRowChanges.forEach { rowChange in
let rowChange = rowChange as! YapDatabaseViewRowChange
let key = rowChange.collectionKey.key
threadViewModelCache[key] = nil
switch rowChange.type {
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic)
case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
default: break
case .delete:
tableView.deleteRows(at: [ rowChange.indexPath! ], with: .automatic)
case .insert:
tableView.insertRows(at: [ rowChange.newIndexPath! ], with: .automatic)
case .update:
tableView.reloadRows(at: [ rowChange.indexPath! ], with: .automatic)
case .move:
// Note: We need to handle the move from the message requests section to the inbox (since
// we are only showing a single row for message requests we need to custom handle this as
// an insert as the change won't be defined correctly)
if rowChange.originalGroup == TSMessageRequestGroup && rowChange.finalGroup == TSInboxGroup {
tableView.insertRows(at: [ rowChange.newIndexPath! ], with: .automatic)
// If that was the last message request then we need to also remove the message request
// row to prevent a crash
if messageRequestCount == 0 {
tableView.deleteRows(at: [ IndexPath(row: 0, section: 0) ], with: .automatic)
}
}
default: break
}
}
tableView.endUpdates()
@ -237,9 +332,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
let rowChange = rowChange as! YapDatabaseViewRowChange
let key = rowChange.collectionKey.key
threadViewModelCache[key] = nil
switch rowChange.type {
case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
default: break
case .move:
// Since we are custom handling this specific movement in the above 'updates' call we need
// to avoid trying to handle it here
if rowChange.originalGroup == TSMessageRequestGroup && rowChange.finalGroup == TSInboxGroup {
return
}
tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
default: break
}
}
tableView.endUpdates()
@ -308,19 +412,104 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
tableView.reloadData()
}
// MARK: Interaction
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section {
case 0:
let viewController: MessageRequestsViewController = MessageRequestsViewController()
self.navigationController?.pushViewController(viewController, animated: true)
return
default:
guard let thread = self.thread(at: indexPath.row) else { return }
show(thread, with: ConversationViewAction.none, highlightedMessageID: nil, animated: true)
}
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
switch indexPath.section {
case 0:
let hide = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_HIDE_TITLE", comment: "")) { [weak self] _, _ in
UserDefaults.standard[.hasHiddenMessageRequests] = true
// Animate the row removal
self?.tableView.beginUpdates()
self?.tableView.deleteRows(at: [indexPath], with: .automatic)
self?.tableView.endUpdates()
}
hide.backgroundColor = Colors.destructive
return [hide]
default:
guard let thread = self.thread(at: indexPath.row) else { return [] }
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
var message = NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", comment: "")
if let thread = thread as? TSGroupThread, thread.isClosedGroup, thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) {
message = NSLocalizedString("admin_group_leave_warning", comment: "")
}
let alert = UIAlertController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", comment: ""), message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { [weak self] _ in
self?.delete(thread)
})
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .default) { _ in })
guard let self = self else { return }
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
let isBlocked = blockingManager.isRecipientIdBlocked(publicKey)
let block = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON", comment: "")) { _, _ in
blockingManager.addBlockedPhoneNumber(publicKey)
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
}
block.backgroundColor = Colors.unimportant
let unblock = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON", comment: "")) { _, _ in
blockingManager.removeBlockedPhoneNumber(publicKey)
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
}
unblock.backgroundColor = Colors.unimportant
return [ delete, (isBlocked ? unblock : block), (isPinned ? unpin : pin) ]
} else {
return [ delete, (isPinned ? unpin : pin) ]
}
}
}
// MARK: - Interaction
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
let seedVC = SeedVC()
let navigationController = OWSNavigationController(rootViewController: seedVC)
present(navigationController, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let thread = self.thread(at: indexPath.row) else { return }
show(thread, with: ConversationViewAction.none, highlightedMessageID: nil, animated: true)
tableView.deselectRow(at: indexPath, animated: true)
}
@objc func show(_ thread: TSThread, with action: ConversationViewAction, highlightedMessageID: String?, animated: Bool) {
DispatchMainThreadSafe {
if let presentedVC = self.presentedViewController {
@ -331,63 +520,6 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
}
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
guard let thread = self.thread(at: indexPath.row) else { return [] }
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
var message = NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", comment: "")
if let thread = thread as? TSGroupThread, thread.isClosedGroup, thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) {
message = NSLocalizedString("admin_group_leave_warning", comment: "")
}
let alert = UIAlertController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", comment: ""), message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { [weak self] _ in
self?.delete(thread)
})
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .default) { _ in })
guard let self = self else { return }
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
let isBlocked = blockingManager.isRecipientIdBlocked(publicKey)
let block = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON", comment: "")) { _, _ in
blockingManager.addBlockedPhoneNumber(publicKey)
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
}
block.backgroundColor = Colors.unimportant
let unblock = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON", comment: "")) { _, _ in
blockingManager.removeBlockedPhoneNumber(publicKey)
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
}
unblock.backgroundColor = Colors.unimportant
return [ delete, (isBlocked ? unblock : block), (isPinned ? unpin : pin) ]
} else {
return [ delete, (isPinned ? unpin : pin) ]
}
}
private func delete(_ thread: TSThread) {
let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)
Storage.write { transaction in
@ -450,8 +582,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
private func thread(at index: Int) -> TSThread? {
var thread: TSThread? = nil
dbConnection.read { transaction in
// Note: Section needs to be '1' as we now have 'TSMessageRequests' as the 0th section
let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
thread = ext.object(atRow: UInt(index), inSection: 1, with: self.threads) as? TSThread
}
return thread
}

View File

@ -0,0 +1,421 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionMessagingKit
@objc
class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
private var threads: YapDatabaseViewMappings!
private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel
private var tableViewTopConstraint: NSLayoutConstraint!
private var messageRequestCount: UInt {
threads.numberOfItems(inGroup: TSMessageRequestGroup)
}
private lazy var dbConnection: YapDatabaseConnection = {
let result = OWSPrimaryStorage.shared().newDatabaseConnection()
result.objectCacheLimit = 500
return result
}()
// MARK: - UI
private lazy var tableView: UITableView = {
let tableView: UITableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.register(MessageRequestsCell.self, forCellReuseIdentifier: MessageRequestsCell.reuseIdentifier)
tableView.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
tableView.dataSource = self
tableView.delegate = self
let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
tableView.showsVerticalScrollIndicator = false
return tableView
}()
private lazy var fadeView: UIView = {
let view: UIView = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isUserInteractionEnabled = false
view.setGradient(Gradients.homeVCFade)
return view
}()
private lazy var clearAllButton: UIButton = {
let button: UIButton = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.clipsToBounds = true
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitle(NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL", comment: ""), for: .normal)
button.setTitleColor(Colors.destructive, for: .normal)
button.setBackgroundImage(
Colors.destructive
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
.toImage(isDarkMode: isDarkMode),
for: .highlighted
)
button.layer.cornerRadius = (NewConversationButtonSet.collapsedButtonSize / 2)
button.layer.borderColor = Colors.destructive.cgColor
button.layer.borderWidth = 1.5
button.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside)
return button
}()
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: ""), hasCustomBackButton: false)
// Threads (part 1)
// Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
dbConnection.beginLongLivedReadTransaction()
// Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting
// the dataSource has the correct data)
view.addSubview(tableView)
view.addSubview(fadeView)
view.addSubview(clearAllButton)
// Notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(handleYapDatabaseModifiedNotification(_:)),
name: .YapDatabaseModified,
object: OWSPrimaryStorage.shared().dbNotificationObject
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleProfileDidChangeNotification(_:)),
name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange),
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleBlockedContactsUpdatedNotification(_:)),
name: .blockedContactsUpdated,
object: nil
)
// Threads (part 2)
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
dbConnection.read { transaction in
self.threads.update(with: transaction) // Perform the initial update
}
setupLayout()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
reload()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Layout
private func setupLayout() {
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing),
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
fadeView.topAnchor.constraint(equalTo: view.topAnchor, constant: (0.15 * view.bounds.height)),
fadeView.leftAnchor.constraint(equalTo: view.leftAnchor),
fadeView.rightAnchor.constraint(equalTo: view.rightAnchor),
fadeView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
clearAllButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
clearAllButton.bottomAnchor.constraint(
equalTo: view.bottomAnchor,
constant: -Values.newConversationButtonBottomOffset // Negative due to how the constraint is set up
),
clearAllButton.widthAnchor.constraint(equalToConstant: 155),
clearAllButton.heightAnchor.constraint(equalToConstant: NewConversationButtonSet.collapsedButtonSize)
])
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Int(messageRequestCount)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
cell.threadViewModel = threadViewModel(at: indexPath.row)
return cell
}
// MARK: - Updating
private func reload() {
AssertIsOnMainThread()
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
dbConnection.read { transaction in
self.threads.update(with: transaction)
}
threadViewModelCache.removeAll()
tableView.reloadData()
clearAllButton.isHidden = (messageRequestCount == 0)
}
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
// NOTE: This code is very finicky and crashes easily. Modify with care.
AssertIsOnMainThread()
// If we don't capture `threads` here, a race condition can occur where the
// `thread.snapshotOfLastUpdate != firstSnapshot - 1` check below evaluates to
// `false`, but `threads` then changes between that check and the
// `ext.getSectionChanges(&sectionChanges, rowChanges: &rowChanges, for: notifications, with: threads)`
// line. This causes `tableView.endUpdates()` to crash with an `NSInternalInconsistencyException`.
let threads = threads!
// Create a stable state for the connection and jump to the latest commit
let notifications = dbConnection.beginLongLivedReadTransaction()
guard !notifications.isEmpty else { return }
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
let hasChanges = ext.hasChanges(forGroup: TSMessageRequestGroup, in: notifications)
guard hasChanges else { return }
if let firstChangeSet = notifications[0].userInfo {
let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64
if threads.snapshotOfLastUpdate != firstSnapshot - 1 {
return reload() // The code below will crash if we try to process multiple commits at once
}
}
var sectionChanges = NSArray()
var rowChanges = NSArray()
ext.getSectionChanges(&sectionChanges, rowChanges: &rowChanges, for: notifications, with: threads)
guard sectionChanges.count > 0 || rowChanges.count > 0 else { return }
tableView.beginUpdates()
rowChanges.forEach { rowChange in
let rowChange = rowChange as! YapDatabaseViewRowChange
let key = rowChange.collectionKey.key
threadViewModelCache[key] = nil
switch rowChange.type {
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic)
case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
default: break
}
}
tableView.endUpdates()
// HACK: Moves can have conflicts with the other 3 types of change.
// Just batch perform all the moves separately to prevent crashing.
// Since all the changes are from the original state to the final state,
// it will still be correct if we pick the moves out.
tableView.beginUpdates()
rowChanges.forEach { rowChange in
let rowChange = rowChange as! YapDatabaseViewRowChange
let key = rowChange.collectionKey.key
threadViewModelCache[key] = nil
switch rowChange.type {
case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
default: break
}
}
tableView.endUpdates()
clearAllButton.isHidden = (messageRequestCount == 0)
}
@objc private func handleProfileDidChangeNotification(_ notification: Notification) {
tableView.reloadData() // TODO: Just reload the affected cell
}
@objc private func handleBlockedContactsUpdatedNotification(_ notification: Notification) {
tableView.reloadData() // TODO: Just reload the affected cell
}
@objc override internal func handleAppModeChangedNotification(_ notification: Notification) {
super.handleAppModeChangedNotification(notification)
let gradient = Gradients.homeVCFade
fadeView.setGradient(gradient) // Re-do the gradient
tableView.reloadData()
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let thread = self.thread(at: indexPath.row) else { return }
let conversationVC = ConversationVC(thread: thread)
self.navigationController?.pushViewController(conversationVC, animated: true)
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
guard let thread = self.thread(at: indexPath.row) else { return [] }
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
var message = NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", comment: "")
if let thread = thread as? TSGroupThread, thread.isClosedGroup, thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) {
message = NSLocalizedString("admin_group_leave_warning", comment: "")
}
let alert = UIAlertController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", comment: ""), message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { [weak self] _ in
self?.delete(thread)
})
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .default) { _ in })
guard let self = self else { return }
self.present(alert, animated: true, completion: nil)
}
delete.backgroundColor = Colors.destructive
return [ delete ]
}
// MARK: - Interaction
private func updateContactAndThread(thread: TSThread, with transaction: YapDatabaseReadWriteTransaction, onComplete: ((Bool) -> ())? = nil) {
guard let contactThread: TSContactThread = thread as? TSContactThread else {
onComplete?(false)
return
}
var needsSync: Bool = false
// Update the contact
let sessionId: String = contactThread.contactSessionID()
if let contact: Contact = Storage.shared.getContact(with: sessionId), (contact.isApproved || !contact.isBlocked) {
contact.isApproved = false
contact.isBlocked = true
Storage.shared.setContact(contact, using: transaction)
needsSync = true
}
// Delete all thread content
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
onComplete?(needsSync)
}
@objc private func clearAllTapped() {
let threadCount: Int = Int(messageRequestCount)
let threads: [TSThread] = (0..<threadCount).compactMap { self.thread(at: $0) }
var needsSync: Bool = false
Storage.write(
with: { [weak self] transaction in
threads.forEach { thread in
if let uniqueId: String = thread.uniqueId {
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
}
self?.updateContactAndThread(thread: thread, with: transaction) { threadNeedsSync in
if threadNeedsSync {
needsSync = true
}
}
}
},
completion: {
// Block all the contacts
threads.forEach { thread in
if let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
}
}
// Force a config sync (must run on the main thread)
if needsSync {
DispatchQueue.main.async {
(UIApplication.shared.delegate as? AppDelegate)?
.forceSyncConfigurationNowIfNeeded()
.retainUntilComplete()
}
}
}
)
}
private func delete(_ thread: TSThread) {
guard let uniqueId: String = thread.uniqueId else { return }
Storage.write(
with: { [weak self] transaction in
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
self?.updateContactAndThread(thread: thread, with: transaction)
},
completion: {
// Block the contact
if let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
}
// Force a config sync (must run on the main thread)
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
}
}
}
)
}
// MARK: - Convenience
private func thread(at index: Int) -> TSThread? {
var thread: TSThread? = nil
dbConnection.read { transaction in
let ext: YapDatabaseViewTransaction? = transaction.ext(TSThreadDatabaseViewExtensionName) as? YapDatabaseViewTransaction
thread = ext?.object(atRow: UInt(index), inSection: 0, with: self.threads) as? TSThread
}
return thread
}
private func threadViewModel(at index: Int) -> ThreadViewModel? {
guard let thread = thread(at: index), let uniqueId: String = thread.uniqueId else { return nil }
if let cachedThreadViewModel = threadViewModelCache[uniqueId] {
return cachedThreadViewModel
}
else {
var threadViewModel: ThreadViewModel? = nil
dbConnection.read { transaction in
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
}
threadViewModelCache[uniqueId] = threadViewModel
return threadViewModel
}
}
}

View File

@ -0,0 +1,134 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
class MessageRequestsCell: UITableViewCell {
static let reuseIdentifier = "MessageRequestsCell"
// MARK: - Initialization
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpViewHierarchy()
setupLayout()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
setupLayout()
}
// MARK: - UI
private let iconContainerView: UIView = {
let view: UIView = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true
view.backgroundColor = Colors.sessionMessageRequestsBubble
view.layer.cornerRadius = (Values.mediumProfilePictureSize / 2)
return view
}()
private let iconImageView: UIImageView = {
let view: UIImageView = UIImageView(image: #imageLiteral(resourceName: "message_requests").withRenderingMode(.alwaysTemplate))
view.translatesAutoresizingMaskIntoConstraints = false
view.tintColor = Colors.sessionMessageRequestsIcon
return view
}()
private let titleLabel: UILabel = {
let label: UILabel = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.font = .boldSystemFont(ofSize: Values.mediumFontSize)
label.text = NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: "")
label.textColor = Colors.sessionMessageRequestsTitle
label.lineBreakMode = .byTruncatingTail
return label
}()
private let unreadCountView: UIView = {
let view: UIView = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true
view.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
view.layer.cornerRadius = (ConversationCell.unreadCountViewSize / 2)
return view
}()
private let unreadCountLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
label.textColor = Colors.text
label.textAlignment = .center
return label
}()
private func setUpViewHierarchy() {
backgroundColor = Colors.cellBackground
selectedBackgroundView = UIView()
selectedBackgroundView?.backgroundColor = Colors.cellSelected
contentView.addSubview(iconContainerView)
contentView.addSubview(titleLabel)
contentView.addSubview(unreadCountView)
iconContainerView.addSubview(iconImageView)
unreadCountView.addSubview(unreadCountLabel)
}
// MARK: - Layout
private func setupLayout() {
NSLayoutConstraint.activate([
contentView.heightAnchor.constraint(equalToConstant: 68),
iconContainerView.leftAnchor.constraint(
equalTo: contentView.leftAnchor,
// Need 'accentLineThickness' to line up correctly with the 'ConversationCell'
constant: (Values.accentLineThickness + Values.mediumSpacing)
),
iconContainerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
iconContainerView.widthAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
iconContainerView.heightAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
iconImageView.centerXAnchor.constraint(equalTo: iconContainerView.centerXAnchor),
iconImageView.centerYAnchor.constraint(equalTo: iconContainerView.centerYAnchor),
iconImageView.widthAnchor.constraint(equalToConstant: 25),
iconImageView.heightAnchor.constraint(equalToConstant: 22),
titleLabel.leftAnchor.constraint(equalTo: iconContainerView.rightAnchor, constant: Values.mediumSpacing),
titleLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing),
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
unreadCountView.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: (Values.smallSpacing / 2)),
unreadCountView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
unreadCountView.widthAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
unreadCountView.heightAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
unreadCountLabel.topAnchor.constraint(equalTo: unreadCountView.topAnchor),
unreadCountLabel.leftAnchor.constraint(equalTo: unreadCountView.leftAnchor),
unreadCountLabel.rightAnchor.constraint(equalTo: unreadCountView.rightAnchor),
unreadCountLabel.bottomAnchor.constraint(equalTo: unreadCountView.bottomAnchor)
])
}
// MARK: - Content
func update(with count: Int) {
unreadCountLabel.text = "\(count)"
unreadCountView.isHidden = (count <= 0)
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "message_requests.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,125 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.001099 cm
0.000000 0.000000 0.000000 scn
21.607576 22.001099 m
3.393768 22.001099 l
1.522707 22.001099 0.000053 20.440092 0.000053 18.520739 c
0.000053 1.402527 l
-0.002093 1.156628 0.061090 0.914558 0.183163 0.701015 c
0.305237 0.487473 0.481828 0.310095 0.694923 0.186979 c
0.900043 0.066381 1.133498 0.002249 1.371506 0.001116 c
1.609514 -0.000017 1.843569 0.061890 2.049829 0.180531 c
6.724900 2.833986 l
7.053913 3.022442 7.426339 3.122168 7.805599 3.123371 c
21.606285 3.123371 l
23.477345 3.123371 25.000000 4.684376 25.000000 6.603730 c
25.000000 18.514294 l
25.001291 20.436871 23.478638 22.001099 21.607576 22.001099 c
h
23.775423 6.603730 m
23.775423 5.359823 22.803120 4.347942 21.607576 4.347942 c
7.806890 4.347942 l
7.216724 4.345131 6.637146 4.191124 6.123580 3.900650 c
1.445283 1.244619 l
1.425066 1.232325 1.401854 1.225824 1.378185 1.225824 c
1.354515 1.225824 1.331300 1.232325 1.311082 1.244619 c
1.283574 1.260406 1.261027 1.283550 1.245980 1.311449 c
1.230933 1.339348 1.223987 1.370895 1.225920 1.402527 c
1.225920 18.520739 l
1.225920 19.764645 2.198225 20.776527 3.393768 20.776527 c
21.607576 20.776527 l
22.803120 20.776527 23.775423 19.761423 23.775423 18.514294 c
23.775423 6.603730 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 0.000000 15.369263 cm
0.000000 0.000000 0.000000 scn
12.584548 1.987679 m
10.633485 1.987679 9.374066 1.053783 9.333419 -1.165269 c
11.107699 -1.165269 l
11.202542 0.052213 11.785154 0.377046 12.584548 0.377046 c
13.383942 0.377046 13.789767 -0.123739 13.789767 -0.718623 c
13.789767 -1.733728 13.467169 -1.936749 11.730955 -3.073668 c
11.730955 -4.643053 l
13.519432 -4.643053 l
13.519432 -3.303759 l
14.521417 -2.802974 15.648563 -1.990891 15.648563 -0.516248 c
15.648563 0.958394 14.589163 1.987679 12.584548 1.987679 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 0.000000 20.065186 cm
0.000000 0.000000 0.000000 scn
13.627180 -10.381148 m
11.568368 -10.381148 l
11.568368 -12.315970 l
13.627180 -12.315970 l
13.627180 -10.381148 l
h
f
n
Q
endstream
endobj
3 0 obj
2088
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 25.000000 22.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002178 00000 n
0000002201 00000 n
0000002374 00000 n
0000002448 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2507
%%EOF

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -583,3 +583,9 @@
"SEARCH_SECTION_MESSAGES" = "Messages";
"SEARCH_SECTION_RECENT" = "Recent";
"RECENT_SEARCH_LAST_MESSAGE_DATETIME" = "last message: %@";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -576,3 +576,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -575,3 +575,9 @@
"system_mode_theme" = "System";
"dark_mode_theme" = "Dark";
"light_mode_theme" = "Light";
"MESSAGE_REQUESTS_TITLE" = "Message Requests";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Clear All";
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"TXT_HIDE_TITLE" = "Hide";
"TXT_DELETE_ACCEPT" = "Accept";

View File

@ -159,6 +159,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
guard !thread.isMuted else { return }
guard thread.isGroupThread() || !thread.isMessageRequest() else { return }
guard let threadId = thread.uniqueId else { return }
let identifier: String = incomingMessage.notificationIdentifier ?? UUID().uuidString

View File

@ -264,6 +264,8 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("vc_settings_notifications_button_title", comment: ""), color: Colors.text, action: #selector(showNotificationSettings)),
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: ""), color: Colors.text, action: #selector(showMessageRequests)),
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("vc_settings_recovery_phrase_button_title", comment: ""), color: Colors.text, action: #selector(showSeed)),
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("vc_settings_clear_all_data_button_title", comment: ""), color: Colors.destructive, action: #selector(clearAllData)),
@ -509,6 +511,11 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
navigationController!.pushViewController(notificationSettingsVC, animated: true)
}
@objc private func showMessageRequests() {
let viewController: MessageRequestsViewController = MessageRequestsViewController()
self.navigationController?.pushViewController(viewController, animated: true)
}
@objc private func showSeed() {
let seedModal = SeedModal()
seedModal.modalPresentationStyle = .overFullScreen

View File

@ -118,7 +118,8 @@ final class ConversationCell : UITableViewCell {
}()
// MARK: Settings
private static let unreadCountViewSize: CGFloat = 20
public static let unreadCountViewSize: CGFloat = 20
private static let statusIndicatorSize: CGFloat = 14
// MARK: Initialization
@ -170,6 +171,7 @@ final class ConversationCell : UITableViewCell {
labelContainerView.axis = .vertical
labelContainerView.alignment = .leading
labelContainerView.spacing = 6
labelContainerView.isUserInteractionEnabled = false
// Main stack view
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, labelContainerView ])
stackView.axis = .horizontal

View File

@ -1,6 +1,7 @@
import UIKit
@objc(LKModal)
class Modal : BaseVC {
class Modal: BaseVC, UIGestureRecognizerDelegate {
private(set) var verticalCenteringConstraint: NSLayoutConstraint!
// MARK: Components
@ -38,9 +39,15 @@ class Modal : BaseVC {
let alpha = isLightMode ? CGFloat(0.1) : Values.highOpacity
view.backgroundColor = UIColor(hex: 0x000000).withAlphaComponent(alpha)
cancelButton.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside)
let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close))
swipeGestureRecognizer.direction = .down
view.addGestureRecognizer(swipeGestureRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(close))
tapGestureRecognizer.delegate = self
view.addGestureRecognizer(tapGestureRecognizer)
setUpViewHierarchy()
}
@ -57,18 +64,17 @@ class Modal : BaseVC {
preconditionFailure("populateContentView() is abstract and must be overridden.")
}
// MARK: Interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: view)
if contentView.frame.contains(location) {
super.touchesBegan(touches, with: event)
} else {
close()
}
}
// MARK: - Interaction
@objc func close() {
dismiss(animated: true, completion: nil)
}
// MARK: - UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let location: CGPoint = touch.location(in: contentView)
return !contentView.point(inside: location, with: nil)
}
}

View File

@ -6,17 +6,29 @@ enum ContactUtilities {
var result: [String] = []
Storage.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSContactThread, thread.shouldBeVisible else { return }
guard
let thread: TSContactThread = object as? TSContactThread,
thread.shouldBeVisible,
Storage.shared.getContact(
with: thread.contactSessionID(),
using: transaction
)?.didApproveMe == true
else {
return
}
result.append(thread.contactSessionID())
}
}
func getDisplayName(for publicKey: String) -> String {
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
}
// Remove the current user
if let index = result.firstIndex(of: getUserHexEncodedPublicKey()) {
result.remove(at: index)
}
// Sort alphabetically
return result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
}

View File

@ -12,6 +12,12 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
@objc public var threadID: String?
/// This flag is used to determine whether we should auto-download files sent by this contact.
@objc public var isTrusted = false
/// This flag is used to determine whether message requests from this contact are approved
@objc public var isApproved = false
/// This flag is used to determine whether message requests from this contact are blocked
@objc public var isBlocked = false
/// This flag is used to determine whether this contact has approved the current users message request
@objc public var didApproveMe = false
// MARK: Name
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
@ -65,6 +71,10 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
if let profilePictureFileName = coder.decodeObject(forKey: "profilePictureFileName") as! String? { self.profilePictureFileName = profilePictureFileName }
if let profileEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profileEncryptionKey = profileEncryptionKey }
if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID }
isApproved = coder.decodeBool(forKey: "isApproved")
isBlocked = coder.decodeBool(forKey: "isBlocked")
didApproveMe = coder.decodeBool(forKey: "didApproveMe")
}
public func encode(with coder: NSCoder) {
@ -76,6 +86,9 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is
coder.encode(profileEncryptionKey, forKey: "profilePictureEncryptionKey")
coder.encode(threadID, forKey: "threadID")
coder.encode(isTrusted, forKey: "isTrusted")
coder.encode(isApproved, forKey: "isApproved")
coder.encode(isBlocked, forKey: "isBlocked")
coder.encode(didApproveMe, forKey: "didApproveMe")
}
// MARK: Equality

View File

@ -8,6 +8,7 @@
NS_ASSUME_NONNULL_BEGIN
extern NSString *const TSInboxGroup;
extern NSString *const TSMessageRequestGroup;
extern NSString *const TSArchiveGroup;
extern NSString *const TSUnreadIncomingMessagesGroup;
extern NSString *const TSSecondaryDevicesGroup;

View File

@ -9,6 +9,7 @@
#import "TSIncomingMessage.h"
#import "TSOutgoingMessage.h"
#import "TSThread.h"
#import "OWSBlockingManager.h"
#import <YapDatabase/YapDatabaseAutoView.h>
#import <YapDatabase/YapDatabaseCrossProcessNotification.h>
#import <YapDatabase/YapDatabaseViewTypes.h>
@ -16,6 +17,7 @@
NS_ASSUME_NONNULL_BEGIN
NSString *const TSInboxGroup = @"TSInboxGroup";
NSString *const TSMessageRequestGroup = @"TSMessageRequestGroup";
NSString *const TSArchiveGroup = @"TSArchiveGroup";
NSString *const TSUnreadIncomingMessagesGroup = @"TSUnreadIncomingMessagesGroup";
@ -234,7 +236,15 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
}
TSThread *thread = (TSThread *)object;
if (thread.shouldBeVisible) {
if (thread.isMessageRequest) {
// Don't show blocked threads at all
if ([[OWSBlockingManager sharedManager] isThreadBlocked: thread]) {
return nil;
}
return TSMessageRequestGroup;
}
else if (thread.shouldBeVisible) {
// Do nothing; we never hide threads that have ever had a message.
} else {
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];

View File

@ -0,0 +1,94 @@
extension ConfigurationMessage {
public static func getCurrent() -> ConfigurationMessage? {
let storage = Storage.shared
guard let user = storage.getUser() else { return nil }
let displayName = user.name
let profilePictureURL = user.profilePictureURL
let profileKey = user.profileEncryptionKey?.keyData
var closedGroups: Set<ClosedGroup> = []
var openGroups: Set<String> = []
var contacts: Set<Contact> = []
var contactCount = 0
Storage.read { transaction in
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSGroupThread else { return }
switch thread.groupModel.groupType {
case .closedGroup:
guard thread.isCurrentUserMemberInGroup() else { return }
let groupID = thread.groupModel.groupId
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
guard storage.isClosedGroup(groupPublicKey), let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else {
return
}
let closedGroup = ClosedGroup(
publicKey: groupPublicKey,
name: thread.groupModel.groupName!,
encryptionKeyPair: encryptionKeyPair,
members: Set(thread.groupModel.groupMemberIds),
admins: Set(thread.groupModel.groupAdminIds),
expirationTimer: thread.disappearingMessagesDuration(with: transaction)
)
closedGroups.insert(closedGroup)
case .openGroup:
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
}
default: break
}
}
var truncatedContacts = storage.getAllContacts()
if truncatedContacts.count > 200 {
truncatedContacts = Set(Array(truncatedContacts)[0..<200])
}
truncatedContacts.forEach { contact in
let publicKey = contact.sessionID
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
guard
let thread = TSContactThread.fetch(uniqueId: threadID, transaction: transaction),
thread.shouldBeVisible &&
!SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey)
else {
return
}
let profilePictureURL = contact.profilePictureURL
let profileKey = contact.profileEncryptionKey?.keyData
let contact = ConfigurationMessage.Contact(
publicKey: publicKey,
displayName: (contact.name ?? publicKey),
profilePictureURL: profilePictureURL,
profileKey: profileKey,
isApproved: contact.isApproved,
isBlocked: contact.isBlocked,
didApproveMe: contact.didApproveMe
)
contacts.insert(contact)
contactCount += 1
}
}
return ConfigurationMessage(
displayName: displayName,
profilePictureURL: profilePictureURL,
profileKey: profileKey,
closedGroups: closedGroups,
openGroups: openGroups,
contacts: contacts
)
}
}

View File

@ -193,14 +193,21 @@ extension ConfigurationMessage {
public var displayName: String?
public var profilePictureURL: String?
public var profileKey: Data?
public var isApproved: Bool
public var isBlocked: Bool
public var didApproveMe: Bool
public var isValid: Bool { publicKey != nil && displayName != nil }
public init(publicKey: String, displayName: String, profilePictureURL: String?, profileKey: Data?) {
public init(publicKey: String, displayName: String, profilePictureURL: String?, profileKey: Data?, isApproved: Bool, isBlocked: Bool, didApproveMe: Bool) {
self.publicKey = publicKey
self.displayName = displayName
self.profilePictureURL = profilePictureURL
self.profileKey = profileKey
self.isApproved = isApproved
self.isBlocked = isBlocked
self.didApproveMe = didApproveMe
}
public required init?(coder: NSCoder) {
@ -210,6 +217,9 @@ extension ConfigurationMessage {
self.displayName = displayName
self.profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String?
self.profileKey = coder.decodeObject(forKey: "profileKey") as! Data?
self.isApproved = (coder.decodeObject(forKey: "isApproved") as? Bool ?? false)
self.isBlocked = (coder.decodeObject(forKey: "isBlocked") as? Bool ?? false)
self.didApproveMe = (coder.decodeObject(forKey: "didApproveMe") as? Bool ?? false)
}
public func encode(with coder: NSCoder) {
@ -217,14 +227,22 @@ extension ConfigurationMessage {
coder.encode(displayName, forKey: "displayName")
coder.encode(profilePictureURL, forKey: "profilePictureURL")
coder.encode(profileKey, forKey: "profileKey")
coder.encode(isApproved, forKey: "isApproved")
coder.encode(isBlocked, forKey: "isBlocked")
coder.encode(didApproveMe, forKey: "didApproveMe")
}
public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> Contact? {
let publicKey = proto.publicKey.toHexString()
let displayName = proto.name
let profilePictureURL = proto.profilePicture
let profileKey = proto.profileKey
let result = Contact(publicKey: publicKey, displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey)
let result: Contact = Contact(
publicKey: proto.publicKey.toHexString(),
displayName: proto.name,
profilePictureURL: proto.profilePicture,
profileKey: proto.profileKey,
isApproved: proto.isApproved,
isBlocked: proto.isBlocked,
didApproveMe: proto.didApproveMe
)
guard result.isValid else { return nil }
return result
}
@ -235,6 +253,11 @@ extension ConfigurationMessage {
let result = SNProtoConfigurationMessageContact.builder(publicKey: Data(hex: publicKey), name: displayName)
if let profilePictureURL = profilePictureURL { result.setProfilePicture(profilePictureURL) }
if let profileKey = profileKey { result.setProfileKey(profileKey) }
result.setIsApproved(isApproved)
result.setIsBlocked(isBlocked)
result.setDidApproveMe(didApproveMe)
do {
return try result.build()
} catch {

View File

@ -0,0 +1,70 @@
import SessionUtilitiesKit
@objc(SNMessageRequestResponse)
public final class MessageRequestResponse: ControlMessage {
public var publicKey: String
public var isApproved: Bool
// MARK: - Initialization
public init(publicKey: String, isApproved: Bool) {
self.publicKey = publicKey
self.isApproved = isApproved
super.init()
}
// MARK: - Coding
public required init?(coder: NSCoder) {
guard let publicKey: String = coder.decodeObject(forKey: "publicKey") as? String else { return nil }
guard let isApproved: Bool = coder.decodeObject(forKey: "isApproved") as? Bool else { return nil }
self.publicKey = publicKey
self.isApproved = isApproved
super.init(coder: coder)
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(publicKey, forKey: "publicKey")
coder.encode(isApproved, forKey: "isApproved")
}
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> MessageRequestResponse? {
guard let messageRequestResponseProto = proto.messageRequestResponse else { return nil }
let publicKey = messageRequestResponseProto.publicKey.toHexString()
let isApproved = messageRequestResponseProto.isApproved
return MessageRequestResponse(publicKey: publicKey, isApproved: isApproved)
}
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
let messageRequestResponseProto = SNProtoMessageRequestResponse.builder(publicKey: Data(hex: publicKey), isApproved: isApproved)
let contentProto = SNProtoContent.builder()
do {
contentProto.setMessageRequestResponse(try messageRequestResponseProto.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct unsend request proto from: \(self).")
return nil
}
}
// MARK: - Description
public override var description: String {
"""
MessageRequestResponse(
publicKey: \(publicKey),
isApproved: \(isApproved)
)
"""
}
}

View File

@ -15,7 +15,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) {
TSInfoMessageTypeGroupCurrentUserLeft,
TSInfoMessageTypeDisappearingMessagesUpdate,
TSInfoMessageTypeScreenshotNotification,
TSInfoMessageTypeMediaSavedNotification
TSInfoMessageTypeMediaSavedNotification,
TSInfoMessageTypeMessageRequestAccepted
};
@property (atomic, readonly) TSInfoMessageType messageType;

View File

@ -112,6 +112,8 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
return NSLocalizedString(@"GROUP_YOU_LEFT", @"");
case TSInfoMessageTypeGroupUpdated:
return _customMessage != nil ? _customMessage : NSLocalizedString(@"GROUP_UPDATED", @"");
case TSInfoMessageTypeMessageRequestAccepted:
return NSLocalizedString(@"MESSAGE_REQUESTS_ACCEPTED", @"");
default:
break;
}

View File

@ -450,6 +450,118 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
#endif
// MARK: - SNProtoMessageRequestResponse
@objc public class SNProtoMessageRequestResponse: NSObject {
// MARK: - SNProtoMessageRequestResponseBuilder
@objc public class func builder(publicKey: Data, isApproved: Bool) -> SNProtoMessageRequestResponseBuilder {
return SNProtoMessageRequestResponseBuilder(publicKey: publicKey, isApproved: isApproved)
}
// asBuilder() constructs a builder that reflects the proto's contents.
@objc public func asBuilder() -> SNProtoMessageRequestResponseBuilder {
let builder = SNProtoMessageRequestResponseBuilder(publicKey: publicKey, isApproved: isApproved)
return builder
}
@objc public class SNProtoMessageRequestResponseBuilder: NSObject {
private var proto = SessionProtos_MessageRequestResponse()
@objc fileprivate override init() {}
@objc fileprivate init(publicKey: Data, isApproved: Bool) {
super.init()
setPublicKey(publicKey)
setIsApproved(isApproved)
}
@objc public func setPublicKey(_ valueParam: Data) {
proto.publicKey = valueParam
}
@objc public func setIsApproved(_ valueParam: Bool) {
proto.isApproved = valueParam
}
@objc public func build() throws -> SNProtoMessageRequestResponse {
return try SNProtoMessageRequestResponse.parseProto(proto)
}
@objc public func buildSerializedData() throws -> Data {
return try SNProtoMessageRequestResponse.parseProto(proto).serializedData()
}
}
fileprivate let proto: SessionProtos_MessageRequestResponse
@objc public let publicKey: Data
@objc public let isApproved: Bool
private init(proto: SessionProtos_MessageRequestResponse,
publicKey: Data,
isApproved: Bool) {
self.proto = proto
self.publicKey = publicKey
self.isApproved = isApproved
}
@objc
public func serializedData() throws -> Data {
return try self.proto.serializedData()
}
@objc public class func parseData(_ serializedData: Data) throws -> SNProtoMessageRequestResponse {
let proto = try SessionProtos_MessageRequestResponse(serializedData: serializedData)
return try parseProto(proto)
}
fileprivate class func parseProto(_ proto: SessionProtos_MessageRequestResponse) throws -> SNProtoMessageRequestResponse {
guard proto.hasPublicKey else {
throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: publicKey")
}
let publicKey = proto.publicKey
guard proto.hasIsApproved else {
throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: isApproved")
}
let isApproved = proto.isApproved
// MARK: - Begin Validation Logic for SNProtoMessageRequestResponse -
// MARK: - End Validation Logic for SNProtoMessageRequestResponse -
let result = SNProtoMessageRequestResponse(proto: proto,
publicKey: publicKey,
isApproved: isApproved)
return result
}
@objc public override var debugDescription: String {
return "\(proto)"
}
}
#if DEBUG
extension SNProtoMessageRequestResponse {
@objc public func serializedDataIgnoringErrors() -> Data? {
return try! self.serializedData()
}
}
extension SNProtoMessageRequestResponse.SNProtoMessageRequestResponseBuilder {
@objc public func buildIgnoringErrors() -> SNProtoMessageRequestResponse? {
return try! self.build()
}
}
#endif
// MARK: - SNProtoContent
@objc public class SNProtoContent: NSObject {
@ -481,6 +593,9 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
if let _value = unsendRequest {
builder.setUnsendRequest(_value)
}
if let _value = messageRequestResponse {
builder.setMessageRequestResponse(_value)
}
return builder
}
@ -514,6 +629,10 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
proto.unsendRequest = valueParam.proto
}
@objc public func setMessageRequestResponse(_ valueParam: SNProtoMessageRequestResponse) {
proto.messageRequestResponse = valueParam.proto
}
@objc public func build() throws -> SNProtoContent {
return try SNProtoContent.parseProto(proto)
}
@ -537,13 +656,16 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
@objc public let unsendRequest: SNProtoUnsendRequest?
@objc public let messageRequestResponse: SNProtoMessageRequestResponse?
private init(proto: SessionProtos_Content,
dataMessage: SNProtoDataMessage?,
receiptMessage: SNProtoReceiptMessage?,
typingMessage: SNProtoTypingMessage?,
configurationMessage: SNProtoConfigurationMessage?,
dataExtractionNotification: SNProtoDataExtractionNotification?,
unsendRequest: SNProtoUnsendRequest?) {
unsendRequest: SNProtoUnsendRequest?,
messageRequestResponse: SNProtoMessageRequestResponse?) {
self.proto = proto
self.dataMessage = dataMessage
self.receiptMessage = receiptMessage
@ -551,6 +673,7 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
self.configurationMessage = configurationMessage
self.dataExtractionNotification = dataExtractionNotification
self.unsendRequest = unsendRequest
self.messageRequestResponse = messageRequestResponse
}
@objc
@ -594,6 +717,11 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
unsendRequest = try SNProtoUnsendRequest.parseProto(proto.unsendRequest)
}
var messageRequestResponse: SNProtoMessageRequestResponse? = nil
if proto.hasMessageRequestResponse {
messageRequestResponse = try SNProtoMessageRequestResponse.parseProto(proto.messageRequestResponse)
}
// MARK: - Begin Validation Logic for SNProtoContent -
// MARK: - End Validation Logic for SNProtoContent -
@ -604,7 +732,8 @@ extension SNProtoUnsendRequest.SNProtoUnsendRequestBuilder {
typingMessage: typingMessage,
configurationMessage: configurationMessage,
dataExtractionNotification: dataExtractionNotification,
unsendRequest: unsendRequest)
unsendRequest: unsendRequest,
messageRequestResponse: messageRequestResponse)
return result
}
@ -2402,6 +2531,9 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
if hasIsBlocked {
builder.setIsBlocked(isBlocked)
}
if hasDidApproveMe {
builder.setDidApproveMe(didApproveMe)
}
return builder
}
@ -2442,6 +2574,10 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
proto.isBlocked = valueParam
}
@objc public func setDidApproveMe(_ valueParam: Bool) {
proto.didApproveMe = valueParam
}
@objc public func build() throws -> SNProtoConfigurationMessageContact {
return try SNProtoConfigurationMessageContact.parseProto(proto)
}
@ -2491,6 +2627,13 @@ extension SNProtoConfigurationMessageClosedGroup.SNProtoConfigurationMessageClos
return proto.hasIsBlocked
}
@objc public var didApproveMe: Bool {
return proto.didApproveMe
}
@objc public var hasDidApproveMe: Bool {
return proto.hasDidApproveMe
}
private init(proto: SessionProtos_ConfigurationMessage.Contact,
publicKey: Data,
name: String) {

View File

@ -229,75 +229,112 @@ struct SessionProtos_UnsendRequest {
fileprivate var _author: String? = nil
}
struct SessionProtos_MessageRequestResponse {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// @required
var publicKey: Data {
get {return _publicKey ?? Data()}
set {_publicKey = newValue}
}
/// Returns true if `publicKey` has been explicitly set.
var hasPublicKey: Bool {return self._publicKey != nil}
/// Clears the value of `publicKey`. Subsequent reads from it will return its default value.
mutating func clearPublicKey() {self._publicKey = nil}
/// @required
var isApproved: Bool {
get {return _isApproved ?? false}
set {_isApproved = newValue}
}
/// Returns true if `isApproved` has been explicitly set.
var hasIsApproved: Bool {return self._isApproved != nil}
/// Clears the value of `isApproved`. Subsequent reads from it will return its default value.
mutating func clearIsApproved() {self._isApproved = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _publicKey: Data? = nil
fileprivate var _isApproved: Bool? = nil
}
struct SessionProtos_Content {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var dataMessage: SessionProtos_DataMessage {
get {return _dataMessage ?? SessionProtos_DataMessage()}
set {_dataMessage = newValue}
get {return _storage._dataMessage ?? SessionProtos_DataMessage()}
set {_uniqueStorage()._dataMessage = newValue}
}
/// Returns true if `dataMessage` has been explicitly set.
var hasDataMessage: Bool {return self._dataMessage != nil}
var hasDataMessage: Bool {return _storage._dataMessage != nil}
/// Clears the value of `dataMessage`. Subsequent reads from it will return its default value.
mutating func clearDataMessage() {self._dataMessage = nil}
mutating func clearDataMessage() {_uniqueStorage()._dataMessage = nil}
var receiptMessage: SessionProtos_ReceiptMessage {
get {return _receiptMessage ?? SessionProtos_ReceiptMessage()}
set {_receiptMessage = newValue}
get {return _storage._receiptMessage ?? SessionProtos_ReceiptMessage()}
set {_uniqueStorage()._receiptMessage = newValue}
}
/// Returns true if `receiptMessage` has been explicitly set.
var hasReceiptMessage: Bool {return self._receiptMessage != nil}
var hasReceiptMessage: Bool {return _storage._receiptMessage != nil}
/// Clears the value of `receiptMessage`. Subsequent reads from it will return its default value.
mutating func clearReceiptMessage() {self._receiptMessage = nil}
mutating func clearReceiptMessage() {_uniqueStorage()._receiptMessage = nil}
var typingMessage: SessionProtos_TypingMessage {
get {return _typingMessage ?? SessionProtos_TypingMessage()}
set {_typingMessage = newValue}
get {return _storage._typingMessage ?? SessionProtos_TypingMessage()}
set {_uniqueStorage()._typingMessage = newValue}
}
/// Returns true if `typingMessage` has been explicitly set.
var hasTypingMessage: Bool {return self._typingMessage != nil}
var hasTypingMessage: Bool {return _storage._typingMessage != nil}
/// Clears the value of `typingMessage`. Subsequent reads from it will return its default value.
mutating func clearTypingMessage() {self._typingMessage = nil}
mutating func clearTypingMessage() {_uniqueStorage()._typingMessage = nil}
var configurationMessage: SessionProtos_ConfigurationMessage {
get {return _configurationMessage ?? SessionProtos_ConfigurationMessage()}
set {_configurationMessage = newValue}
get {return _storage._configurationMessage ?? SessionProtos_ConfigurationMessage()}
set {_uniqueStorage()._configurationMessage = newValue}
}
/// Returns true if `configurationMessage` has been explicitly set.
var hasConfigurationMessage: Bool {return self._configurationMessage != nil}
var hasConfigurationMessage: Bool {return _storage._configurationMessage != nil}
/// Clears the value of `configurationMessage`. Subsequent reads from it will return its default value.
mutating func clearConfigurationMessage() {self._configurationMessage = nil}
mutating func clearConfigurationMessage() {_uniqueStorage()._configurationMessage = nil}
var dataExtractionNotification: SessionProtos_DataExtractionNotification {
get {return _dataExtractionNotification ?? SessionProtos_DataExtractionNotification()}
set {_dataExtractionNotification = newValue}
get {return _storage._dataExtractionNotification ?? SessionProtos_DataExtractionNotification()}
set {_uniqueStorage()._dataExtractionNotification = newValue}
}
/// Returns true if `dataExtractionNotification` has been explicitly set.
var hasDataExtractionNotification: Bool {return self._dataExtractionNotification != nil}
var hasDataExtractionNotification: Bool {return _storage._dataExtractionNotification != nil}
/// Clears the value of `dataExtractionNotification`. Subsequent reads from it will return its default value.
mutating func clearDataExtractionNotification() {self._dataExtractionNotification = nil}
mutating func clearDataExtractionNotification() {_uniqueStorage()._dataExtractionNotification = nil}
var unsendRequest: SessionProtos_UnsendRequest {
get {return _unsendRequest ?? SessionProtos_UnsendRequest()}
set {_unsendRequest = newValue}
get {return _storage._unsendRequest ?? SessionProtos_UnsendRequest()}
set {_uniqueStorage()._unsendRequest = newValue}
}
/// Returns true if `unsendRequest` has been explicitly set.
var hasUnsendRequest: Bool {return self._unsendRequest != nil}
var hasUnsendRequest: Bool {return _storage._unsendRequest != nil}
/// Clears the value of `unsendRequest`. Subsequent reads from it will return its default value.
mutating func clearUnsendRequest() {self._unsendRequest = nil}
mutating func clearUnsendRequest() {_uniqueStorage()._unsendRequest = nil}
var messageRequestResponse: SessionProtos_MessageRequestResponse {
get {return _storage._messageRequestResponse ?? SessionProtos_MessageRequestResponse()}
set {_uniqueStorage()._messageRequestResponse = newValue}
}
/// Returns true if `messageRequestResponse` has been explicitly set.
var hasMessageRequestResponse: Bool {return _storage._messageRequestResponse != nil}
/// Clears the value of `messageRequestResponse`. Subsequent reads from it will return its default value.
mutating func clearMessageRequestResponse() {_uniqueStorage()._messageRequestResponse = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _dataMessage: SessionProtos_DataMessage? = nil
fileprivate var _receiptMessage: SessionProtos_ReceiptMessage? = nil
fileprivate var _typingMessage: SessionProtos_TypingMessage? = nil
fileprivate var _configurationMessage: SessionProtos_ConfigurationMessage? = nil
fileprivate var _dataExtractionNotification: SessionProtos_DataExtractionNotification? = nil
fileprivate var _unsendRequest: SessionProtos_UnsendRequest? = nil
fileprivate var _storage = _StorageClass.defaultInstance
}
struct SessionProtos_KeyPair {
@ -1096,6 +1133,16 @@ struct SessionProtos_ConfigurationMessage {
/// Clears the value of `isBlocked`. Subsequent reads from it will return its default value.
mutating func clearIsBlocked() {self._isBlocked = nil}
/// added for msg requests
var didApproveMe: Bool {
get {return _didApproveMe ?? false}
set {_didApproveMe = newValue}
}
/// Returns true if `didApproveMe` has been explicitly set.
var hasDidApproveMe: Bool {return self._didApproveMe != nil}
/// Clears the value of `didApproveMe`. Subsequent reads from it will return its default value.
mutating func clearDidApproveMe() {self._didApproveMe = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
@ -1106,6 +1153,7 @@ struct SessionProtos_ConfigurationMessage {
fileprivate var _profileKey: Data? = nil
fileprivate var _isApproved: Bool? = nil
fileprivate var _isBlocked: Bool? = nil
fileprivate var _didApproveMe: Bool? = nil
}
init() {}
@ -1625,24 +1673,16 @@ extension SessionProtos_UnsendRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes
}
}
extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".Content"
extension SessionProtos_MessageRequestResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".MessageRequestResponse"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "dataMessage"),
5: .same(proto: "receiptMessage"),
6: .same(proto: "typingMessage"),
7: .same(proto: "configurationMessage"),
8: .same(proto: "dataExtractionNotification"),
9: .same(proto: "unsendRequest"),
1: .same(proto: "publicKey"),
2: .same(proto: "isApproved"),
]
public var isInitialized: Bool {
if let v = self._dataMessage, !v.isInitialized {return false}
if let v = self._receiptMessage, !v.isInitialized {return false}
if let v = self._typingMessage, !v.isInitialized {return false}
if let v = self._configurationMessage, !v.isInitialized {return false}
if let v = self._dataExtractionNotification, !v.isInitialized {return false}
if let v = self._unsendRequest, !v.isInitialized {return false}
if self._publicKey == nil {return false}
if self._isApproved == nil {return false}
return true
}
@ -1652,12 +1692,8 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularMessageField(value: &self._dataMessage) }()
case 5: try { try decoder.decodeSingularMessageField(value: &self._receiptMessage) }()
case 6: try { try decoder.decodeSingularMessageField(value: &self._typingMessage) }()
case 7: try { try decoder.decodeSingularMessageField(value: &self._configurationMessage) }()
case 8: try { try decoder.decodeSingularMessageField(value: &self._dataExtractionNotification) }()
case 9: try { try decoder.decodeSingularMessageField(value: &self._unsendRequest) }()
case 1: try { try decoder.decodeSingularBytesField(value: &self._publicKey) }()
case 2: try { try decoder.decodeSingularBoolField(value: &self._isApproved) }()
default: break
}
}
@ -1668,34 +1704,147 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._dataMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
try { if let v = self._publicKey {
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
} }()
try { if let v = self._receiptMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
} }()
try { if let v = self._typingMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
} }()
try { if let v = self._configurationMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
} }()
try { if let v = self._dataExtractionNotification {
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
} }()
try { if let v = self._unsendRequest {
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
try { if let v = self._isApproved {
try visitor.visitSingularBoolField(value: v, fieldNumber: 2)
} }()
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SessionProtos_MessageRequestResponse, rhs: SessionProtos_MessageRequestResponse) -> Bool {
if lhs._publicKey != rhs._publicKey {return false}
if lhs._isApproved != rhs._isApproved {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".Content"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "dataMessage"),
5: .same(proto: "receiptMessage"),
6: .same(proto: "typingMessage"),
7: .same(proto: "configurationMessage"),
8: .same(proto: "dataExtractionNotification"),
9: .same(proto: "unsendRequest"),
10: .same(proto: "messageRequestResponse"),
]
fileprivate class _StorageClass {
var _dataMessage: SessionProtos_DataMessage? = nil
var _receiptMessage: SessionProtos_ReceiptMessage? = nil
var _typingMessage: SessionProtos_TypingMessage? = nil
var _configurationMessage: SessionProtos_ConfigurationMessage? = nil
var _dataExtractionNotification: SessionProtos_DataExtractionNotification? = nil
var _unsendRequest: SessionProtos_UnsendRequest? = nil
var _messageRequestResponse: SessionProtos_MessageRequestResponse? = nil
static let defaultInstance = _StorageClass()
private init() {}
init(copying source: _StorageClass) {
_dataMessage = source._dataMessage
_receiptMessage = source._receiptMessage
_typingMessage = source._typingMessage
_configurationMessage = source._configurationMessage
_dataExtractionNotification = source._dataExtractionNotification
_unsendRequest = source._unsendRequest
_messageRequestResponse = source._messageRequestResponse
}
}
fileprivate mutating func _uniqueStorage() -> _StorageClass {
if !isKnownUniquelyReferenced(&_storage) {
_storage = _StorageClass(copying: _storage)
}
return _storage
}
public var isInitialized: Bool {
return withExtendedLifetime(_storage) { (_storage: _StorageClass) in
if let v = _storage._dataMessage, !v.isInitialized {return false}
if let v = _storage._receiptMessage, !v.isInitialized {return false}
if let v = _storage._typingMessage, !v.isInitialized {return false}
if let v = _storage._configurationMessage, !v.isInitialized {return false}
if let v = _storage._dataExtractionNotification, !v.isInitialized {return false}
if let v = _storage._unsendRequest, !v.isInitialized {return false}
if let v = _storage._messageRequestResponse, !v.isInitialized {return false}
return true
}
}
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
_ = _uniqueStorage()
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularMessageField(value: &_storage._dataMessage) }()
case 5: try { try decoder.decodeSingularMessageField(value: &_storage._receiptMessage) }()
case 6: try { try decoder.decodeSingularMessageField(value: &_storage._typingMessage) }()
case 7: try { try decoder.decodeSingularMessageField(value: &_storage._configurationMessage) }()
case 8: try { try decoder.decodeSingularMessageField(value: &_storage._dataExtractionNotification) }()
case 9: try { try decoder.decodeSingularMessageField(value: &_storage._unsendRequest) }()
case 10: try { try decoder.decodeSingularMessageField(value: &_storage._messageRequestResponse) }()
default: break
}
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = _storage._dataMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
} }()
try { if let v = _storage._receiptMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
} }()
try { if let v = _storage._typingMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
} }()
try { if let v = _storage._configurationMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
} }()
try { if let v = _storage._dataExtractionNotification {
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
} }()
try { if let v = _storage._unsendRequest {
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
} }()
try { if let v = _storage._messageRequestResponse {
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
} }()
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: SessionProtos_Content, rhs: SessionProtos_Content) -> Bool {
if lhs._dataMessage != rhs._dataMessage {return false}
if lhs._receiptMessage != rhs._receiptMessage {return false}
if lhs._typingMessage != rhs._typingMessage {return false}
if lhs._configurationMessage != rhs._configurationMessage {return false}
if lhs._dataExtractionNotification != rhs._dataExtractionNotification {return false}
if lhs._unsendRequest != rhs._unsendRequest {return false}
if lhs._storage !== rhs._storage {
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
let _storage = _args.0
let rhs_storage = _args.1
if _storage._dataMessage != rhs_storage._dataMessage {return false}
if _storage._receiptMessage != rhs_storage._receiptMessage {return false}
if _storage._typingMessage != rhs_storage._typingMessage {return false}
if _storage._configurationMessage != rhs_storage._configurationMessage {return false}
if _storage._dataExtractionNotification != rhs_storage._dataExtractionNotification {return false}
if _storage._unsendRequest != rhs_storage._unsendRequest {return false}
if _storage._messageRequestResponse != rhs_storage._messageRequestResponse {return false}
return true
}
if !storagesAreEqual {return false}
}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@ -2552,6 +2701,7 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
4: .same(proto: "profileKey"),
5: .same(proto: "isApproved"),
6: .same(proto: "isBlocked"),
7: .same(proto: "didApproveMe"),
]
public var isInitialized: Bool {
@ -2572,6 +2722,7 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
case 4: try { try decoder.decodeSingularBytesField(value: &self._profileKey) }()
case 5: try { try decoder.decodeSingularBoolField(value: &self._isApproved) }()
case 6: try { try decoder.decodeSingularBoolField(value: &self._isBlocked) }()
case 7: try { try decoder.decodeSingularBoolField(value: &self._didApproveMe) }()
default: break
}
}
@ -2600,6 +2751,9 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
try { if let v = self._isBlocked {
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
} }()
try { if let v = self._didApproveMe {
try visitor.visitSingularBoolField(value: v, fieldNumber: 7)
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2610,6 +2764,7 @@ extension SessionProtos_ConfigurationMessage.Contact: SwiftProtobuf.Message, Swi
if lhs._profileKey != rhs._profileKey {return false}
if lhs._isApproved != rhs._isApproved {return false}
if lhs._isBlocked != rhs._isBlocked {return false}
if lhs._didApproveMe != rhs._didApproveMe {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View File

@ -41,6 +41,13 @@ message UnsendRequest {
required string author = 2;
}
message MessageRequestResponse {
// @required
required bytes publicKey = 1; // The public key of the contact that was approved
// @required
required bool isApproved = 2; // Whether the request was approved
}
message Content {
optional DataMessage dataMessage = 1;
optional ReceiptMessage receiptMessage = 5;
@ -48,6 +55,7 @@ message Content {
optional ConfigurationMessage configurationMessage = 7;
optional DataExtractionNotification dataExtractionNotification = 8;
optional UnsendRequest unsendRequest = 9;
optional MessageRequestResponse messageRequestResponse = 10;
}
message KeyPair {
@ -181,6 +189,7 @@ message ConfigurationMessage {
optional bytes profileKey = 4;
optional bool isApproved = 5; // added for msg requests
optional bool isBlocked = 6; // added for msg requests
optional bool didApproveMe = 7; // added for msg requests
}
repeated ClosedGroup closedGroups = 1;

View File

@ -16,6 +16,7 @@ extension MessageReceiver {
case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction)
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
case let message as UnsendRequest: handleUnsendRequest(message, using: transaction)
case let message as MessageRequestResponse: handleMessageRequestResponse(message, using: transaction)
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
default: fatalError()
}
@ -204,10 +205,23 @@ extension MessageReceiver {
if let profileKey = contactInfo.profileKey { contact.profileEncryptionKey = OWSAES256Key(data: profileKey) }
contact.profilePictureURL = contactInfo.profilePictureURL
contact.name = contactInfo.displayName
contact.isApproved = contactInfo.isApproved
contact.isBlocked = contactInfo.isBlocked
contact.didApproveMe = contactInfo.didApproveMe
Storage.shared.setContact(contact, using: transaction)
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
thread.shouldBeVisible = true
thread.save(with: transaction)
// Make sure to sync the contact blocked state
if contact.isBlocked != OWSBlockingManager.shared().isRecipientIdBlocked(contact.sessionID) {
if contact.isBlocked {
OWSBlockingManager.shared().addBlockedPhoneNumber(contact.sessionID)
}
else {
OWSBlockingManager.shared().removeBlockedPhoneNumber(contact.sessionID)
}
}
}
// Closed groups
let allClosedGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
@ -339,6 +353,21 @@ extension MessageReceiver {
// Mark previous messages as read if there is a sync message
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: tsOutgoingMessage.sortId, thread: thread)
}
// Update the contact's approval status of the current user if needed (if we are getting messages from
// them outside of a group then we can assume they have approved the current user)
//
// Note: This is to resolve a rare edge-case where a conversation was started with a user on an old
// version of the app and their message request approval state was set via a migration rather than
// by using the approval process
if !isGroup, let senderSessionId: String = message.sender {
updateContactApprovalStatusOfMeIfNeeded(
contactSessionId: senderSessionId,
didApproveMe: true,
using: transaction
)
}
// 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 }
@ -427,10 +456,26 @@ extension MessageReceiver {
private static func handleNewClosedGroup(groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: [String], admins: [String], expirationTimer: UInt32, messageSentTimestamp: UInt64, using transaction: Any) {
let transaction = transaction as! YapDatabaseReadWriteTransaction
// With new closed groups we only want to create them if the admin creating the closed group is an
// approved contact (to prevent spam via closed groups getting around message requests if users are
// on old or modified clients)
var hasApprovedAdmin: Bool = false
for adminId in admins {
if let contact: Contact = Storage.shared.getContact(with: adminId), contact.isApproved {
hasApprovedAdmin = true
break
}
}
guard hasApprovedAdmin else { return }
// Create the group
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
let thread: TSGroupThread
if let t = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) {
thread = t
thread.setGroupModel(group, with: transaction)
@ -439,18 +484,24 @@ extension MessageReceiver {
if !storage.isClosedGroup(groupPublicKey) {
storage.setZombieMembers(for: groupPublicKey, to: [], using: transaction)
}
} else {
}
else {
thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
thread.save(with: transaction)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: messageSentTimestamp, in: thread, messageType: .groupCreated)
infoMessage.save(with: transaction)
}
let isExpirationTimerEnabled = (expirationTimer > 0)
let expirationTimerDuration = (isExpirationTimerEnabled ? expirationTimer : 24 * 60 * 60)
let configuration = OWSDisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: isExpirationTimerEnabled,
durationSeconds: expirationTimerDuration)
let configuration = OWSDisappearingMessagesConfiguration(
threadId: thread.uniqueId!,
enabled: isExpirationTimerEnabled,
durationSeconds: expirationTimerDuration
)
configuration.save(with: transaction)
// Add the group to the user's set of public keys to poll for
Storage.shared.addClosedGroupPublicKey(groupPublicKey, using: transaction)
// Store the key pair
@ -694,4 +745,53 @@ extension MessageReceiver {
// Perform the update
update(groupID, thread, group)
}
// MARK: - Message Requests
private static func updateContactApprovalStatusOfMeIfNeeded(contactSessionId: String, didApproveMe: Bool, using transaction: Any) {
let userPublicKey = getUserHexEncodedPublicKey()
// Only make changes if the contact isn't the current user, we can retrieve the contact and
// the 'didApproveMe' flag is currently false
guard contactSessionId != userPublicKey else { return }
guard let contact: Contact = Storage.shared.getContact(with: contactSessionId) else { return }
guard !contact.didApproveMe else { return }
contact.didApproveMe = didApproveMe
Storage.shared.setContact(contact, using: transaction)
// Need to force a config sync to ensure all devices know the contact approve
// communication with the user (Note: This logic should match the behaviour
// in AppDelegate.forceSyncConfigurationNowIfNeeded())
guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent() else {
return
}
let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey)
MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete()
}
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) {
let userPublicKey = getUserHexEncodedPublicKey()
// Ignore messages which aren't targeted at the current user
guard message.publicKey == userPublicKey else { return }
guard let senderId: String = message.sender else { return }
// Get the existing thead and notify the user
if let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction, let thread: TSContactThread = TSContactThread.getWithContactSessionID(senderId, transaction: transaction) {
let infoMessage = TSInfoMessage(
timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()),
in: thread,
messageType: .messageRequestAccepted
)
infoMessage.save(with: transaction)
}
updateContactApprovalStatusOfMeIfNeeded(
contactSessionId: senderId,
didApproveMe: message.isApproved,
using: transaction
)
}
}

View File

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

View File

@ -57,6 +57,19 @@ NSString *const TSContactThreadPrefix = @"c";
return @[ self.contactSessionID ];
}
- (BOOL)isMessageRequest {
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
return (
self.shouldBeVisible &&
!self.isNoteToSelf && (
contact == nil ||
!contact.isApproved
)
);
}
- (BOOL)isGroupThread
{
return NO;

View File

@ -47,6 +47,13 @@ BOOL IsNoteToSelfEnabled(void);
- (BOOL)isNoteToSelf;
/**
* Whether the thread is a message request.
*
* @return YES if the combination of thread and contact approval means this thread should appear in the message requests section, NO otherwise.
*/
- (BOOL)isMessageRequest;
#pragma mark Interactions
- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction usingBlock:(void (^)(TSInteraction *interaction, BOOL *stop))block;

View File

@ -132,6 +132,11 @@ BOOL IsNoteToSelfEnabled(void)
return [self.contactSessionID isEqual:[SNGeneralUtilities getUserPublicKey]];
}
// Override in ContactThread
- (BOOL)isMessageRequest {
return NO;
}
#pragma mark To be subclassed.
- (BOOL)isGroupThread {

View File

@ -52,8 +52,8 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
snippet = tsMessage.previewText(with: transaction).filterForDisplay?.replacingMentions(for: threadID, using: transaction)
?? "You've got a new message"
if let tsIncomingMessage = tsMessage as? TSIncomingMessage {
if thread.isMuted {
// Ignore PNs if the thread is muted
if thread.isMuted || !thread.isMessageRequest() {
// Ignore PNs if the thread is muted or the thread is a message request
return self.completeSilenty()
}
if let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction), let group = thread as? TSGroupThread,

View File

@ -43,4 +43,8 @@ public final class Colors : NSObject {
@objc public static var pathsBuilding: UIColor { UIColor(named: "session_paths_building")! }
@objc public static var pinIcon: UIColor { UIColor(named: "session_pin_icon")! }
@objc public static var sessionHeading: UIColor { UIColor(named: "session_heading")! }
@objc public static var sessionMessageRequestsBubble: UIColor { UIColor(named: "session_message_requests_bubble")! }
@objc public static var sessionMessageRequestsIcon: UIColor { UIColor(named: "session_message_requests_icon")! }
@objc public static var sessionMessageRequestsTitle: UIColor { UIColor(named: "session_message_requests_title")! }
@objc public static var sessionMessageRequestsInfoText: UIColor { UIColor(named: "session_message_requests_info_text")! }
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x58",
"green" : "0x58",
"red" : "0x58"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x43",
"green" : "0x43",
"red" : "0x43"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xAD",
"green" : "0xAD",
"red" : "0xAD"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x9F",
"green" : "0x9F",
"red" : "0x9F"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xAD",
"green" : "0xAD",
"red" : "0xAD"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "96",
"green" : "96",
"red" : "96"
"blue" : "0x60",
"green" : "0x60",
"red" : "0x60"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "179",
"green" : "179",
"red" : "179"
"blue" : "0xB3",
"green" : "0xB3",
"red" : "0xB3"
}
},
"idiom" : "universal"

View File

@ -7,6 +7,7 @@ public enum SNUserDefaults {
case hasViewedSeed
case hasSeenLinkPreviewSuggestion
case isUsingFullAPNs
case hasHiddenMessageRequests
}
public enum Date : Swift.String {

View File

@ -0,0 +1,56 @@
@objc(SNMessageRequestsMigration)
public class MessageRequestsMigration : OWSDatabaseMigration {
@objc
class func migrationId() -> String {
return "002"
}
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
self.doMigrationAsync(completion: completion)
}
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
var contacts: Set<Contact> = Set()
var threads: [TSThread] = []
TSThread.enumerateCollectionObjects { object, _ in
guard let thread: TSThread = object as? TSThread else { return }
if let contactThread: TSContactThread = thread as? TSContactThread {
let sessionId: String = contactThread.contactSessionID()
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
contact.isApproved = true
contact.didApproveMe = true
contacts.insert(contact)
}
}
else if let groupThread: TSGroupThread = thread as? TSGroupThread, groupThread.isClosedGroup {
let groupAdmins: [String] = groupThread.groupModel.groupAdminIds
groupAdmins.forEach { sessionId in
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
contact.isApproved = true
contact.didApproveMe = true
contacts.insert(contact)
}
}
}
threads.append(thread)
}
Storage.write(with: { transaction in
contacts.forEach { contact in
Storage.shared.setContact(contact, using: transaction)
}
threads.forEach { thread in
thread.save(with: transaction)
}
self.save(with: transaction) // Intentionally capture self
}, completion: {
completion()
})
}
}

View File

@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
- (NSArray<OWSDatabaseMigration *> *)allMigrations
{
return @[
[SNMessageRequestsMigration new],
[SNContactsMigration new]
];
}

View File

@ -1,52 +0,0 @@
extension ConfigurationMessage {
public static func getCurrent() -> ConfigurationMessage? {
let storage = Storage.shared
guard let user = storage.getUser() else { return nil }
let displayName = user.name
let profilePictureURL = user.profilePictureURL
let profileKey = user.profileEncryptionKey?.keyData
var closedGroups: Set<ClosedGroup> = []
var openGroups: Set<String> = []
var contacts: Set<Contact> = []
var contactCount = 0
Storage.read { transaction in
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSGroupThread else { return }
switch thread.groupModel.groupType {
case .closedGroup:
guard thread.isCurrentUserMemberInGroup() else { return }
let groupID = thread.groupModel.groupId
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
guard storage.isClosedGroup(groupPublicKey),
let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { return }
let closedGroup = ClosedGroup(publicKey: groupPublicKey, name: thread.groupModel.groupName!, encryptionKeyPair: encryptionKeyPair,
members: Set(thread.groupModel.groupMemberIds), admins: Set(thread.groupModel.groupAdminIds), expirationTimer: thread.disappearingMessagesDuration(with: transaction))
closedGroups.insert(closedGroup)
case .openGroup:
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
}
default: break
}
}
var truncatedContacts = storage.getAllContacts()
if truncatedContacts.count > 200 { truncatedContacts = Set(Array(truncatedContacts)[0..<200]) }
truncatedContacts.forEach { contact in
let publicKey = contact.sessionID
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
guard let thread = TSContactThread.fetch(uniqueId: threadID, transaction: transaction), thread.shouldBeVisible
&& !SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey) else { return }
let profilePictureURL = contact.profilePictureURL
let profileKey = contact.profileEncryptionKey?.keyData
let contact = ConfigurationMessage.Contact(publicKey: publicKey, displayName: contact.name ?? publicKey,
profilePictureURL: profilePictureURL, profileKey: profileKey)
contacts.insert(contact)
contactCount += 1
}
}
return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
}
}

View File

@ -75,8 +75,11 @@ NS_ASSUME_NONNULL_BEGIN
// FIXME: Confusingly, `allGroups` includes contact threads as well
for (NSString *groupID in allGroups) {
TSThread *thread = [TSThread fetchObjectWithUniqueID:groupID transaction:transaction];
if (thread.isMuted) { continue; }
if (thread.isMuted || !thread.isMessageRequest) { continue; }
BOOL isGroupThread = thread.isGroupThread;
[unreadMessages enumerateKeysAndObjectsInGroup:groupID
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {

View File

@ -0,0 +1,27 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIColor
public extension UIColor {
func toImage(isDarkMode: Bool) -> UIImage {
let bounds: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
if #available(iOS 13.0, *) {
rendererContext.cgContext
.setFillColor(
self.resolvedColor(
// Note: This is needed for '.cgColor' to support dark mode
with: UITraitCollection(userInterfaceStyle: isDarkMode ? .dark : .light)
).cgColor
)
}
else {
rendererContext.cgContext.setFillColor(self.cgColor)
}
rendererContext.cgContext.fill(bounds)
}
}
}

View File

@ -47,11 +47,15 @@ NS_ASSUME_NONNULL_BEGIN
OWSLogInfo(@"Checking migrations. currentVersion: %@, lastRanVersion: %@", currentVersion, previousVersion);
if (!previousVersion) {
OWSLogInfo(@"No previous version found. Probably first launch since install - nothing to migrate.");
OWSDatabaseMigrationRunner *runner = [[OWSDatabaseMigrationRunner alloc] init];
// Note: We need to run the migrations here anyway to ensure that they don't run on subsequent launches
// and result in unexpected data changes (eg. 'MessageRequestsMigration' auto-approves all threads
// if this happens on the 2nd launch then any threads created during the 1st launch which haven't
// been approved would get auto-approved, allowing the user to use contacts which haven't approved
// comms to appear as options when creating closed groups)
OWSLogInfo(@"No previous version found. Probably first launch since install - running migrations so they don't run on second launch.");
dispatch_async(dispatch_get_main_queue(), ^{
completion();
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:completion];
});
return;
}