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:
parent
0f20c37afa
commit
9db5083cc5
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(§ionChanges, 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
|
||||
}
|
||||
|
|
|
@ -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(§ionChanges, 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(§ionChanges, 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
12
Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json
vendored
Normal file
12
Session/Meta/Images.xcassets/Session/message_requests.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "message_requests.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
125
Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf
vendored
Normal file
125
Session/Meta/Images.xcassets/Session/message_requests.imageset/message_requests.pdf
vendored
Normal 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
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
|
@ -15,7 +15,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) {
|
|||
TSInfoMessageTypeGroupCurrentUserLeft,
|
||||
TSInfoMessageTypeDisappearingMessagesUpdate,
|
||||
TSInfoMessageTypeScreenshotNotification,
|
||||
TSInfoMessageTypeMediaSavedNotification
|
||||
TSInfoMessageTypeMediaSavedNotification,
|
||||
TSInfoMessageTypeMessageRequestAccepted
|
||||
};
|
||||
|
||||
@property (atomic, readonly) TSInfoMessageType messageType;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")! }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -7,6 +7,7 @@ public enum SNUserDefaults {
|
|||
case hasViewedSeed
|
||||
case hasSeenLinkPreviewSuggestion
|
||||
case isUsingFullAPNs
|
||||
case hasHiddenMessageRequests
|
||||
}
|
||||
|
||||
public enum Date : Swift.String {
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (NSArray<OWSDatabaseMigration *> *)allMigrations
|
||||
{
|
||||
return @[
|
||||
[SNMessageRequestsMigration new],
|
||||
[SNContactsMigration new]
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)]) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue